249 lines
7.3 KiB
Crystal
249 lines
7.3 KiB
Crystal
# 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 |element|
|
|
static_literal?(element)
|
|
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
|