# Utility module for Ameba's rules. module Ameba::AST::Util # Returns tuple with two bool flags: # # 1. is *node* a literal? # 2. can *node* be proven static? protected def literal_kind?(node) : {Bool, Bool} case node when Crystal::NilLiteral, Crystal::BoolLiteral, Crystal::NumberLiteral, Crystal::CharLiteral, Crystal::StringLiteral, Crystal::SymbolLiteral, Crystal::RegexLiteral, Crystal::ProcLiteral, Crystal::MacroLiteral {true, true} when Crystal::RangeLiteral {true, static_literal?(node.from) && static_literal?(node.to)} when Crystal::ArrayLiteral, Crystal::TupleLiteral {true, node.elements.all? do |el| static_literal?(el) end} when Crystal::HashLiteral {true, node.entries.all? do |entry| static_literal?(entry.key) && static_literal?(entry.value) end} when Crystal::NamedTupleLiteral {true, node.entries.all? do |entry| static_literal?(entry.value) end} else {false, false} end end # Returns `true` if current `node` is a static literal, `false` otherwise. def static_literal?(node) : Bool is_literal, is_static = literal_kind?(node) is_literal && is_static end # Returns `true` if current `node` is a dynamic literal, `false` otherwise. def dynamic_literal?(node) : Bool is_literal, is_static = literal_kind?(node) is_literal && !is_static end # Returns `true` if current `node` is a literal, `false` otherwise. def literal?(node) : Bool is_literal, _ = literal_kind?(node) is_literal end # Returns `true` if current `node` is a `Crystal::Path` # matching given *name*, `false` otherwise. def path_named?(node, name) : Bool node.is_a?(Crystal::Path) && name == node.names.join("::") end # Returns a source code for the current node. # This method uses `node.location` and `node.end_location` # to determine and cut a piece of source of the node. def node_source(node, code_lines) loc, end_loc = node.location, node.end_location return unless loc && end_loc source_between(loc, end_loc, code_lines) end # Returns the source code from *loc* to *end_loc* (inclusive). def source_between(loc, end_loc, code_lines) : String? line, column = loc.line_number - 1, loc.column_number - 1 end_line, end_column = end_loc.line_number - 1, end_loc.column_number - 1 node_lines = code_lines[line..end_line] first_line, last_line = node_lines[0]?, node_lines[-1]? return if first_line.nil? || last_line.nil? return if first_line.size < column # compiler reports incorrect location node_lines[0] = first_line.sub(0...column, "") if line == end_line # one line end_column = end_column - column last_line = node_lines[0] end return if last_line.size < end_column + 1 node_lines[-1] = last_line.sub(end_column + 1...last_line.size, "") node_lines.join('\n') end # Returns `true` if node is a flow command, `false` otherwise. # Node represents a flow command if it is a control expression, # or special call node that interrupts execution (i.e. raise, exit, abort). def flow_command?(node, in_loop) case node when Crystal::Return true when Crystal::Break, Crystal::Next in_loop when Crystal::Call raise?(node) || exit?(node) || abort?(node) else false end end # Returns `true` if node is a flow expression, `false` if not. # Node represents a flow expression if it is full-filled by a flow command. # # For example, this node is a flow expression, because each branch contains # a flow command `return`: # # ``` # if a > 0 # return :positive # elsif a < 0 # return :negative # else # return :zero # end # ``` # # This node is a not a flow expression: # # ``` # if a > 0 # return :positive # end # ``` # # That's because not all branches return(i.e. `else` is missing). def flow_expression?(node, in_loop = false) return true if flow_command? node, in_loop case node when Crystal::If, Crystal::Unless flow_expressions? [node.then, node.else], in_loop when Crystal::BinaryOp flow_expression? node.left, in_loop when Crystal::Case flow_expressions? [node.whens, node.else].flatten, in_loop when Crystal::ExceptionHandler flow_expressions? [node.else || node.body, node.rescues].flatten, in_loop when Crystal::While, Crystal::Until flow_expression? node.body, in_loop when Crystal::Rescue, Crystal::When flow_expression? node.body, in_loop when Crystal::Expressions node.expressions.any? { |exp| flow_expression? exp, in_loop } else false end end private def flow_expressions?(nodes, in_loop) nodes.all? { |exp| flow_expression? exp, in_loop } end # Returns `true` if node represents `raise` method call. def raise?(node) node.is_a?(Crystal::Call) && node.name == "raise" && node.args.size == 1 && node.obj.nil? end # Returns `true` if node represents `exit` method call. def exit?(node) node.is_a?(Crystal::Call) && node.name == "exit" && node.args.size <= 1 && node.obj.nil? end # Returns `true` if node represents `abort` method call. def abort?(node) node.is_a?(Crystal::Call) && node.name == "abort" && node.args.size <= 2 && node.obj.nil? end # Returns `true` if node represents a loop. def loop?(node) case node when Crystal::While, Crystal::Until true when Crystal::Call node.name == "loop" && node.args.size == 0 && node.obj.nil? else false end end # Returns the exp code of a control expression. # Wraps implicit tuple literal with curly brackets (e.g. multi-return). def control_exp_code(node : Crystal::ControlExpression, code_lines) return unless exp = node.exp return unless exp_code = node_source(exp, code_lines) return exp_code unless exp.is_a?(Crystal::TupleLiteral) && exp_code[0] != '{' return unless exp_start = exp.elements.first.location return unless exp_end = exp.end_location "{#{source_between(exp_start, exp_end, code_lines)}}" end # Returns `nil` if *node* does not contain a name. def name_location(node) if loc = node.name_location return loc end return node.var.location if node.is_a?(Crystal::TypeDeclaration) || node.is_a?(Crystal::UninitializedVar) return unless node.responds_to?(:name) && (name = node.name) return unless name.is_a?(Crystal::ASTNode) name.location end # Returns zero if *node* does not contain a name. def name_size(node) unless (size = node.name_size).zero? return size end return 0 unless node.responds_to?(:name) && (name = node.name) case name when Crystal::ASTNode then name.name_size when Crystal::Token::Kind then name.to_s.size # Crystal::MagicConstant else name.size end end # Returns `nil` if *node* does not contain a name. # # NOTE: Use this instead of `Crystal::Call#name_end_location` to avoid an # off-by-one error. def name_end_location(node) return unless loc = name_location(node) return if (size = name_size(node)).zero? loc.adjust(column_number: size - 1) end end