/
traversal.rb
182 lines (157 loc) · 6.55 KB
/
traversal.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# 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)
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