# frozen_string_literal: true
module RuboCop
module AST
# Provides methods for traversing an AST.
# Does not transform an AST; for that, use Parser::AST::Processor.
# Override methods to perform custom processing. Remember to call `super`
# if you want to recursively process descendant nodes.
module Traversal
# Only for debugging.
# @api private
class DebugError < RuntimeError
end
TYPE_TO_METHOD = Hash.new { |h, type| h[type] = :"on_#{type}" }
def walk(node)
return if node.nil?
send(TYPE_TO_METHOD[node.type], node)
nil
end
# @api private
module CallbackCompiler
SEND = 'send(TYPE_TO_METHOD[child.type], child)'
assign_code = 'child = node.children[%<index>i]'
code = "#{assign_code}\n#{SEND}"
TEMPLATE = {
skip: '',
always: code,
nil?: "#{code} if child"
}.freeze
def def_callback(type, *signature,
arity: signature.size..signature.size,
arity_check: ENV.fetch('RUBOCOP_DEBUG', nil) && self.arity_check(arity),
body: self.body(signature, arity_check))
type, *aliases = type
lineno = caller_locations(1, 1).first.lineno
module_eval(<<~RUBY, __FILE__, lineno) # rubocop:disable Style/EvalWithLocation
def on_#{type}(node) # def on_send(node)
#{body} # # body ...
nil # nil
end # end
RUBY
aliases.each do |m|
alias_method "on_#{m}", "on_#{type}"
end
end
def body(signature, prelude)
signature
.map.with_index do |arg, i|
TEMPLATE[arg].gsub('%<index>i', i.to_s)
end
.unshift(prelude)
.join("\n")
end
def arity_check(range)
<<~RUBY
n = node.children.size
raise DebugError, [
'Expected #{range} children, got',
n, 'for', node.inspect
].join(' ') unless (#{range}).cover?(node.children.size)
RUBY
end
end
private_constant :CallbackCompiler
extend CallbackCompiler
send_code = CallbackCompiler::SEND
### arity == 0
no_children = %i[true false nil self cbase zsuper redo retry
forward_args forwarded_args match_nil_pattern
forward_arg forwarded_restarg forwarded_kwrestarg
lambda empty_else kwnilarg
__FILE__ __LINE__ __ENCODING__]
### arity == 0..1
opt_symbol_child = %i[restarg kwrestarg]
opt_node_child = %i[splat kwsplat match_rest]
### arity == 1
literal_child = %i[int float complex
rational str sym lvar
ivar cvar gvar nth_ref back_ref
arg blockarg shadowarg
kwarg match_var]
many_symbol_children = %i[regopt]
node_child = %i[not match_current_line defined?
arg_expr pin if_guard unless_guard
match_with_trailing_comma]
node_or_nil_child = %i[block_pass preexe postexe]
NO_CHILD_NODES = (no_children + opt_symbol_child + literal_child).to_set.freeze
private_constant :NO_CHILD_NODES # Used by Commissioner
### arity > 1
symbol_then_opt_node = %i[lvasgn ivasgn cvasgn gvasgn]
symbol_then_node_or_nil = %i[optarg kwoptarg]
node_then_opt_node = %i[while until module sclass]
### variable arity
many_node_children = %i[dstr dsym xstr regexp array hash pair
mlhs masgn or_asgn and_asgn rasgn mrasgn
undef alias args super yield or and
while_post until_post iflipflop eflipflop
match_with_lvasgn begin kwbegin return
in_match match_alt break next
match_as array_pattern array_pattern_with_tail
hash_pattern const_pattern find_pattern
index indexasgn procarg0 kwargs]
many_opt_node_children = %i[case rescue resbody ensure for when
case_match in_pattern irange erange
match_pattern match_pattern_p]
### Callbacks for above
def_callback no_children
def_callback opt_symbol_child, :skip, arity: 0..1
def_callback opt_node_child, :nil?, arity: 0..1
def_callback literal_child, :skip
def_callback node_child, :always
def_callback node_or_nil_child, :nil?
def_callback symbol_then_opt_node, :skip, :nil?, arity: 1..2
def_callback symbol_then_node_or_nil, :skip, :nil?
def_callback node_then_opt_node, :always, :nil?
def_callback many_symbol_children, :skip, arity_check: nil
def_callback many_node_children, body: <<~RUBY
node.children.each { |child| #{send_code} }
RUBY
def_callback many_opt_node_children,
body: <<~RUBY
node.children.each { |child| #{send_code} if child }
RUBY
### Other particular cases
def_callback :const, :nil?, :skip
def_callback :casgn, :nil?, :skip, :nil?, arity: 2..3
def_callback :class, :always, :nil?, :nil?
def_callback :def, :skip, :always, :nil?
def_callback :op_asgn, :always, :skip, :always
def_callback :if, :always, :nil?, :nil?
def_callback :block, :always, :always, :nil?
def_callback :numblock, :always, :skip, :nil?
def_callback :defs, :always, :skip, :always, :nil?
def_callback %i[send csend], body: <<~RUBY
node.children.each_with_index do |child, i|
next if i == 1
#{send_code} if child
end
RUBY
### generic processing of any other node (forward compatibility)
defined = instance_methods(false)
.grep(/^on_/)
.map { |s| s.to_s[3..].to_sym } # :on_foo => :foo
to_define = ::Parser::Meta::NODE_TYPES.to_a
to_define -= defined
to_define -= %i[numargs ident] # transient
to_define -= %i[blockarg_expr restarg_expr] # obsolete
to_define -= %i[objc_kwarg objc_restarg objc_varargs] # mac_ruby
def_callback to_define, body: <<~RUBY
node.children.each do |child|
next unless child.class == Node
#{send_code}
end
RUBY
MISSING = to_define if ENV['RUBOCOP_DEBUG']
end
end
end