rubocop-ast 0.0.3 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +21 -4
- data/lib/rubocop/ast.rb +9 -1
- data/lib/rubocop/ast/builder.rb +8 -1
- data/lib/rubocop/ast/ext/range.rb +28 -0
- data/lib/rubocop/ast/ext/set.rb +12 -0
- data/lib/rubocop/ast/node.rb +81 -10
- data/lib/rubocop/ast/node/array_node.rb +2 -8
- data/lib/rubocop/ast/node/block_node.rb +1 -1
- data/lib/rubocop/ast/node/break_node.rb +1 -6
- data/lib/rubocop/ast/node/case_match_node.rb +3 -9
- data/lib/rubocop/ast/node/case_node.rb +13 -9
- data/lib/rubocop/ast/node/const_node.rb +65 -0
- data/lib/rubocop/ast/node/def_node.rb +5 -24
- data/lib/rubocop/ast/node/defined_node.rb +2 -0
- data/lib/rubocop/ast/node/float_node.rb +1 -0
- data/lib/rubocop/ast/node/forward_args_node.rb +15 -0
- data/lib/rubocop/ast/node/hash_node.rb +21 -8
- data/lib/rubocop/ast/node/if_node.rb +7 -14
- data/lib/rubocop/ast/node/index_node.rb +48 -0
- data/lib/rubocop/ast/node/indexasgn_node.rb +50 -0
- data/lib/rubocop/ast/node/int_node.rb +1 -0
- data/lib/rubocop/ast/node/lambda_node.rb +65 -0
- data/lib/rubocop/ast/node/mixin/method_dispatch_node.rb +2 -8
- data/lib/rubocop/ast/node/mixin/method_identifier_predicates.rb +99 -3
- data/lib/rubocop/ast/node/mixin/parameterized_node.rb +56 -0
- data/lib/rubocop/ast/node/next_node.rb +12 -0
- data/lib/rubocop/ast/node/pair_node.rb +2 -2
- data/lib/rubocop/ast/node/regexp_node.rb +56 -0
- data/lib/rubocop/ast/node/resbody_node.rb +21 -0
- data/lib/rubocop/ast/node/rescue_node.rb +49 -0
- data/lib/rubocop/ast/node/return_node.rb +1 -13
- data/lib/rubocop/ast/node/send_node.rb +9 -2
- data/lib/rubocop/ast/node/super_node.rb +2 -0
- data/lib/rubocop/ast/node/when_node.rb +3 -9
- data/lib/rubocop/ast/node/yield_node.rb +2 -0
- data/lib/rubocop/ast/node_pattern.rb +184 -115
- data/lib/rubocop/ast/processed_source.rb +98 -16
- data/lib/rubocop/ast/traversal.rb +6 -4
- data/lib/rubocop/ast/version.rb +1 -1
- metadata +16 -9
- data/lib/rubocop/ast/node/retry_node.rb +0 -17
@@ -2,8 +2,11 @@
|
|
2
2
|
|
3
3
|
module RuboCop
|
4
4
|
module AST
|
5
|
+
# Requires implementing `arguments`.
|
6
|
+
#
|
5
7
|
# Common functionality for nodes that are parameterized:
|
6
8
|
# `send`, `super`, `zsuper`, `def`, `defs`
|
9
|
+
# and (modern only): `index`, `indexasgn`, `lambda`
|
7
10
|
module ParameterizedNode
|
8
11
|
# Checks whether this node's arguments are wrapped in parentheses.
|
9
12
|
#
|
@@ -56,6 +59,59 @@ module RuboCop
|
|
56
59
|
arguments? &&
|
57
60
|
(last_argument.block_pass_type? || last_argument.blockarg_type?)
|
58
61
|
end
|
62
|
+
|
63
|
+
# A specialized `ParameterizedNode` for node that have a single child
|
64
|
+
# containing either `nil`, an argument, or a `begin` node with all the
|
65
|
+
# arguments
|
66
|
+
module WrappedArguments
|
67
|
+
include ParameterizedNode
|
68
|
+
# @return [Array] The arguments of the node.
|
69
|
+
def arguments
|
70
|
+
first = children.first
|
71
|
+
if first&.begin_type?
|
72
|
+
first.children
|
73
|
+
else
|
74
|
+
children
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# A specialized `ParameterizedNode`.
|
80
|
+
# Requires implementing `first_argument_index`
|
81
|
+
# Implements `arguments` as `children[first_argument_index..-1]`
|
82
|
+
# and optimizes other calls
|
83
|
+
module RestArguments
|
84
|
+
include ParameterizedNode
|
85
|
+
# @return [Array<Node>] arguments, if any
|
86
|
+
def arguments
|
87
|
+
children[first_argument_index..-1].freeze
|
88
|
+
end
|
89
|
+
|
90
|
+
# A shorthand for getting the first argument of the node.
|
91
|
+
# Equivalent to `arguments.first`.
|
92
|
+
#
|
93
|
+
# @return [Node, nil] the first argument of the node,
|
94
|
+
# or `nil` if there are no arguments
|
95
|
+
def first_argument
|
96
|
+
children[first_argument_index]
|
97
|
+
end
|
98
|
+
|
99
|
+
# A shorthand for getting the last argument of the node.
|
100
|
+
# Equivalent to `arguments.last`.
|
101
|
+
#
|
102
|
+
# @return [Node, nil] the last argument of the node,
|
103
|
+
# or `nil` if there are no arguments
|
104
|
+
def last_argument
|
105
|
+
children[-1] if arguments?
|
106
|
+
end
|
107
|
+
|
108
|
+
# Checks whether this node has any arguments.
|
109
|
+
#
|
110
|
+
# @return [Boolean] whether this node has any arguments
|
111
|
+
def arguments?
|
112
|
+
children.size > first_argument_index
|
113
|
+
end
|
114
|
+
end
|
59
115
|
end
|
60
116
|
end
|
61
117
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module AST
|
5
|
+
# A node extension for `next` nodes. This will be used in place of a
|
6
|
+
# plain node when the builder constructs the AST, making its methods
|
7
|
+
# available to all `next` nodes within RuboCop.
|
8
|
+
class NextNode < Node
|
9
|
+
include ParameterizedNode::WrappedArguments
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -32,7 +32,7 @@ module RuboCop
|
|
32
32
|
#
|
33
33
|
# @param [Boolean] with_spacing whether to include spacing
|
34
34
|
# @return [String] the delimiter of the `pair`
|
35
|
-
def delimiter(with_spacing
|
35
|
+
def delimiter(*deprecated, with_spacing: deprecated.first)
|
36
36
|
if with_spacing
|
37
37
|
hash_rocket? ? SPACED_HASH_ROCKET : SPACED_COLON
|
38
38
|
else
|
@@ -44,7 +44,7 @@ module RuboCop
|
|
44
44
|
#
|
45
45
|
# @param [Boolean] with_spacing whether to include spacing
|
46
46
|
# @return [String] the inverse delimiter of the `pair`
|
47
|
-
def inverse_delimiter(with_spacing
|
47
|
+
def inverse_delimiter(*deprecated, with_spacing: deprecated.first)
|
48
48
|
if with_spacing
|
49
49
|
hash_rocket? ? SPACED_COLON : SPACED_HASH_ROCKET
|
50
50
|
else
|
@@ -31,6 +31,62 @@ module RuboCop
|
|
31
31
|
def content
|
32
32
|
children.select(&:str_type?).map(&:str_content).join
|
33
33
|
end
|
34
|
+
|
35
|
+
# @return [Bool] if the regexp is a /.../ literal
|
36
|
+
def slash_literal?
|
37
|
+
loc.begin.source == '/'
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Bool] if the regexp is a %r{...} literal (using any delimiters)
|
41
|
+
def percent_r_literal?
|
42
|
+
!slash_literal?
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [String] the regexp delimiters (without %r)
|
46
|
+
def delimiters
|
47
|
+
[loc.begin.source[-1], loc.end.source[0]]
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Bool] if char is one of the delimiters
|
51
|
+
def delimiter?(char)
|
52
|
+
delimiters.include?(char)
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [Bool] if regexp contains interpolation
|
56
|
+
def interpolation?
|
57
|
+
children.any?(&:begin_type?)
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Bool] if regexp uses the multiline regopt
|
61
|
+
def multiline_mode?
|
62
|
+
regopt_include?(:m)
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [Bool] if regexp uses the extended regopt
|
66
|
+
def extended?
|
67
|
+
regopt_include?(:x)
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Bool] if regexp uses the ignore-case regopt
|
71
|
+
def ignore_case?
|
72
|
+
regopt_include?(:i)
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [Bool] if regexp uses the single-interpolation regopt
|
76
|
+
def single_interpolation?
|
77
|
+
regopt_include?(:o)
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [Bool] if regexp uses the no-encoding regopt
|
81
|
+
def no_encoding?
|
82
|
+
regopt_include?(:n)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def regopt_include?(option)
|
88
|
+
regopt.children.include?(option)
|
89
|
+
end
|
34
90
|
end
|
35
91
|
end
|
36
92
|
end
|
@@ -13,12 +13,33 @@ module RuboCop
|
|
13
13
|
node_parts[2]
|
14
14
|
end
|
15
15
|
|
16
|
+
# Returns an array of all the exceptions in the `rescue` clause.
|
17
|
+
#
|
18
|
+
# @return [Array<Node>] an array of exception nodes
|
19
|
+
def exceptions
|
20
|
+
exceptions_node = node_parts[0]
|
21
|
+
if exceptions_node.nil?
|
22
|
+
[]
|
23
|
+
elsif exceptions_node.array_type?
|
24
|
+
exceptions_node.values
|
25
|
+
else
|
26
|
+
[exceptions_node]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
16
30
|
# Returns the exception variable of the `rescue` clause.
|
17
31
|
#
|
18
32
|
# @return [Node, nil] The exception variable of the `resbody`.
|
19
33
|
def exception_variable
|
20
34
|
node_parts[1]
|
21
35
|
end
|
36
|
+
|
37
|
+
# Returns the index of the `resbody` branch within the exception handling statement.
|
38
|
+
#
|
39
|
+
# @return [Integer] the index of the `resbody` branch
|
40
|
+
def branch_index
|
41
|
+
parent.resbody_branches.index(self)
|
42
|
+
end
|
22
43
|
end
|
23
44
|
end
|
24
45
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module AST
|
5
|
+
# A node extension for `rescue` nodes. This will be used in place of a
|
6
|
+
# plain node when the builder constructs the AST, making its methods
|
7
|
+
# available to all `rescue` nodes within RuboCop.
|
8
|
+
class RescueNode < Node
|
9
|
+
# Returns the body of the rescue node.
|
10
|
+
#
|
11
|
+
# @return [Node, nil] The body of the rescue node.
|
12
|
+
def body
|
13
|
+
node_parts[0]
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns an array of all the rescue branches in the exception handling statement.
|
17
|
+
#
|
18
|
+
# @return [Array<ResbodyNode>] an array of `resbody` nodes
|
19
|
+
def resbody_branches
|
20
|
+
node_parts[1...-1]
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns an array of all the rescue branches in the exception handling statement.
|
24
|
+
#
|
25
|
+
# @return [Array<Node, nil>] an array of the bodies of the rescue branches
|
26
|
+
# and the else (if any). Note that these bodies could be nil.
|
27
|
+
def branches
|
28
|
+
bodies = resbody_branches.map(&:body)
|
29
|
+
bodies.push(else_branch) if else?
|
30
|
+
bodies
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns the else branch of the exception handling statement, if any.
|
34
|
+
#
|
35
|
+
# @return [Node] the else branch node of the exception handling statement
|
36
|
+
# @return [nil] if the exception handling statement does not have an else branch.
|
37
|
+
def else_branch
|
38
|
+
node_parts[-1]
|
39
|
+
end
|
40
|
+
|
41
|
+
# Checks whether this exception handling statement has an `else` branch.
|
42
|
+
#
|
43
|
+
# @return [Boolean] whether the exception handling statement has an `else` branch
|
44
|
+
def else?
|
45
|
+
loc.else
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -6,19 +6,7 @@ module RuboCop
|
|
6
6
|
# plain node when the builder constructs the AST, making its methods
|
7
7
|
# available to all `return` nodes within RuboCop.
|
8
8
|
class ReturnNode < Node
|
9
|
-
include
|
10
|
-
include ParameterizedNode
|
11
|
-
|
12
|
-
# Returns the arguments of the `return`.
|
13
|
-
#
|
14
|
-
# @return [Array] The arguments of the `return`.
|
15
|
-
def arguments
|
16
|
-
if node_parts.one? && node_parts.first.begin_type?
|
17
|
-
node_parts.first.children
|
18
|
-
else
|
19
|
-
node_parts
|
20
|
-
end
|
21
|
-
end
|
9
|
+
include ParameterizedNode::WrappedArguments
|
22
10
|
end
|
23
11
|
end
|
24
12
|
end
|
@@ -6,12 +6,19 @@ module RuboCop
|
|
6
6
|
# node when the builder constructs the AST, making its methods available
|
7
7
|
# to all `send` nodes within RuboCop.
|
8
8
|
class SendNode < Node
|
9
|
-
include ParameterizedNode
|
9
|
+
include ParameterizedNode::RestArguments
|
10
10
|
include MethodDispatchNode
|
11
11
|
|
12
12
|
def_node_matcher :attribute_accessor?, <<~PATTERN
|
13
|
-
(send nil? ${:attr_reader :attr_writer :attr_accessor :attr} $...)
|
13
|
+
[(send nil? ${:attr_reader :attr_writer :attr_accessor :attr} $...)
|
14
|
+
(_ _ _ _ ...)]
|
14
15
|
PATTERN
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def first_argument_index
|
20
|
+
2
|
21
|
+
end
|
15
22
|
end
|
16
23
|
end
|
17
24
|
end
|
@@ -13,17 +13,11 @@ module RuboCop
|
|
13
13
|
node_parts[0...-1]
|
14
14
|
end
|
15
15
|
|
16
|
-
#
|
17
|
-
|
18
|
-
#
|
19
|
-
# @return [self] if a block is given
|
20
|
-
# @return [Enumerator] if no block is given
|
21
|
-
def each_condition
|
16
|
+
# @deprecated Use `conditions.each`
|
17
|
+
def each_condition(&block)
|
22
18
|
return conditions.to_enum(__method__) unless block_given?
|
23
19
|
|
24
|
-
conditions.each
|
25
|
-
yield condition
|
26
|
-
end
|
20
|
+
conditions.each(&block)
|
27
21
|
|
28
22
|
self
|
29
23
|
end
|
@@ -70,13 +70,23 @@ module RuboCop
|
|
70
70
|
# '(send %1 _)' # % stands for a parameter which must be supplied to
|
71
71
|
# # #match at matching time
|
72
72
|
# # it will be compared to the corresponding value in
|
73
|
-
# # the AST using
|
73
|
+
# # the AST using #=== so you can pass Procs, Regexp,
|
74
|
+
# # etc. in addition to Nodes or literals.
|
75
|
+
# # `Array#===` will never match a node element, but
|
76
|
+
# # `Set#===` is an alias to `Set#include?` (Ruby 2.5+
|
77
|
+
# # only), and so can be very useful to match within
|
78
|
+
# # many possible literals / Nodes.
|
74
79
|
# # a bare '%' is the same as '%1'
|
75
80
|
# # the number of extra parameters passed to #match
|
76
81
|
# # must equal the highest % value in the pattern
|
77
82
|
# # for consistency, %0 is the 'root node' which is
|
78
83
|
# # passed as the 1st argument to #match, where the
|
79
84
|
# # matching process starts
|
85
|
+
# '(send _ %named)' # arguments can also be passed as named
|
86
|
+
# # parameters (see `%1`)
|
87
|
+
# # Note that the macros `def_node_matcher` and
|
88
|
+
# # `def_node_search` accept default values for these.
|
89
|
+
# '(send _ %CONST)' # the named constant will act like `%1` and `%named`.
|
80
90
|
# '^^send' # each ^ ascends one level in the AST
|
81
91
|
# # so this matches against the grandparent node
|
82
92
|
# '`send' # descends any number of level in the AST
|
@@ -86,6 +96,10 @@ module RuboCop
|
|
86
96
|
# # if that returns a truthy value, the match succeeds
|
87
97
|
# 'equal?(%1)' # predicates can be given 1 or more extra args
|
88
98
|
# '#method(%0, 1)' # funcalls can also be given 1 or more extra args
|
99
|
+
# # These arguments can be patterns themselves, in
|
100
|
+
# # which case a matcher responding to === will be
|
101
|
+
# # passed.
|
102
|
+
# '# comment' # comments are accepted at the end of lines
|
89
103
|
#
|
90
104
|
# You can nest arbitrarily deep:
|
91
105
|
#
|
@@ -100,11 +114,6 @@ module RuboCop
|
|
100
114
|
# and so on. Therefore, if you add methods which are named like
|
101
115
|
# `#prefix_type?` to the AST node class, then 'prefix' will become usable as
|
102
116
|
# a pattern.
|
103
|
-
#
|
104
|
-
# Also note that if you need a "guard clause" to protect against possible nils
|
105
|
-
# in a certain place in the AST, you can do it like this: `[!nil <pattern>]`
|
106
|
-
#
|
107
|
-
# The compiler code is very simple; don't be afraid to read through it!
|
108
117
|
class NodePattern
|
109
118
|
# @private
|
110
119
|
Invalid = Class.new(StandardError)
|
@@ -114,17 +123,23 @@ module RuboCop
|
|
114
123
|
class Compiler
|
115
124
|
SYMBOL = %r{:(?:[\w+@*/?!<>=~|%^-]+|\[\]=?)}.freeze
|
116
125
|
IDENTIFIER = /[a-zA-Z_][a-zA-Z0-9_-]*/.freeze
|
126
|
+
COMMENT = /#\s.*$/.freeze
|
127
|
+
|
117
128
|
META = Regexp.union(
|
118
129
|
%w"( ) { } [ ] $< < > $... $ ! ^ ` ... + * ?"
|
119
130
|
).freeze
|
120
131
|
NUMBER = /-?\d+(?:\.\d+)?/.freeze
|
121
132
|
STRING = /".+?"/.freeze
|
122
|
-
METHOD_NAME = /\#?#{IDENTIFIER}[
|
133
|
+
METHOD_NAME = /\#?#{IDENTIFIER}[!?]?\(?/.freeze
|
134
|
+
PARAM_CONST = /%[A-Z:][a-zA-Z_:]+/.freeze
|
135
|
+
KEYWORD_NAME = /%[a-z_]+/.freeze
|
123
136
|
PARAM_NUMBER = /%\d*/.freeze
|
124
137
|
|
125
|
-
SEPARATORS =
|
126
|
-
|
127
|
-
|
138
|
+
SEPARATORS = /\s+/.freeze
|
139
|
+
ONLY_SEPARATOR = /\A#{SEPARATORS}\Z/.freeze
|
140
|
+
|
141
|
+
TOKENS = Regexp.union(META, PARAM_CONST, KEYWORD_NAME, PARAM_NUMBER, NUMBER,
|
142
|
+
METHOD_NAME, SYMBOL, STRING)
|
128
143
|
|
129
144
|
TOKEN = /\G(?:#{SEPARATORS}|#{TOKENS}|.)/.freeze
|
130
145
|
|
@@ -135,6 +150,8 @@ module RuboCop
|
|
135
150
|
FUNCALL = /\A\##{METHOD_NAME}/.freeze
|
136
151
|
LITERAL = /\A(?:#{SYMBOL}|#{NUMBER}|#{STRING})\Z/.freeze
|
137
152
|
PARAM = /\A#{PARAM_NUMBER}\Z/.freeze
|
153
|
+
CONST = /\A#{PARAM_CONST}\Z/.freeze
|
154
|
+
KEYWORD = /\A#{KEYWORD_NAME}\Z/.freeze
|
138
155
|
CLOSING = /\A(?:\)|\}|\])\Z/.freeze
|
139
156
|
|
140
157
|
REST = '...'
|
@@ -149,6 +166,7 @@ module RuboCop
|
|
149
166
|
CUR_NODE = "#{CUR_PLACEHOLDER} node@@@"
|
150
167
|
CUR_ELEMENT = "#{CUR_PLACEHOLDER} element@@@"
|
151
168
|
SEQ_HEAD_GUARD = '@@@seq guard head@@@'
|
169
|
+
MULTIPLE_CUR_PLACEHOLDER = /#{CUR_PLACEHOLDER}.*#{CUR_PLACEHOLDER}/.freeze
|
152
170
|
|
153
171
|
line = __LINE__
|
154
172
|
ANY_ORDER_TEMPLATE = ERB.new <<~RUBY.gsub("-%>\n", '%>')
|
@@ -185,21 +203,26 @@ module RuboCop
|
|
185
203
|
RUBY
|
186
204
|
REPEATED_TEMPLATE.location = [__FILE__, line + 1]
|
187
205
|
|
188
|
-
def initialize(str,
|
206
|
+
def initialize(str, root = 'node0', node_var = root)
|
189
207
|
@string = str
|
190
|
-
|
208
|
+
# For def_node_matcher, root == node_var
|
209
|
+
# For def_node_search, root is the root node to search on,
|
210
|
+
# and node_var is the current descendant being searched.
|
211
|
+
@root = root
|
212
|
+
@node_var = node_var
|
191
213
|
|
192
214
|
@temps = 0 # avoid name clashes between temp variables
|
193
215
|
@captures = 0 # number of captures seen
|
194
216
|
@unify = {} # named wildcard -> temp variable
|
195
217
|
@params = 0 # highest % (param) number seen
|
196
|
-
|
218
|
+
@keywords = Set[] # keyword parameters seen
|
219
|
+
run
|
197
220
|
end
|
198
221
|
|
199
|
-
def run
|
222
|
+
def run
|
200
223
|
@tokens = Compiler.tokens(@string)
|
201
224
|
|
202
|
-
@match_code = with_context(compile_expr, node_var, use_temp_node: false)
|
225
|
+
@match_code = with_context(compile_expr, @node_var, use_temp_node: false)
|
203
226
|
@match_code.prepend("(captures = Array.new(#{@captures})) && ") \
|
204
227
|
if @captures.positive?
|
205
228
|
|
@@ -219,6 +242,10 @@ module RuboCop
|
|
219
242
|
# CUR_NODE: Ruby code that evaluates to an AST node
|
220
243
|
# CUR_ELEMENT: Either the node or the type if in first element of
|
221
244
|
# a sequence (aka seq_head, e.g. "(seq_head first_node_arg ...")
|
245
|
+
if (atom = compile_atom(token))
|
246
|
+
return atom_to_expr(atom)
|
247
|
+
end
|
248
|
+
|
222
249
|
case token
|
223
250
|
when '(' then compile_seq
|
224
251
|
when '{' then compile_union
|
@@ -227,14 +254,10 @@ module RuboCop
|
|
227
254
|
when '$' then compile_capture
|
228
255
|
when '^' then compile_ascend
|
229
256
|
when '`' then compile_descend
|
230
|
-
when WILDCARD then
|
257
|
+
when WILDCARD then compile_new_wildcard(token[1..-1])
|
231
258
|
when FUNCALL then compile_funcall(token)
|
232
|
-
when LITERAL then compile_literal(token)
|
233
259
|
when PREDICATE then compile_predicate(token)
|
234
260
|
when NODE then compile_nodetype(token)
|
235
|
-
when PARAM then compile_param(token[1..-1])
|
236
|
-
when CLOSING then fail_due_to("#{token} in invalid position")
|
237
|
-
when nil then fail_due_to('pattern ended prematurely')
|
238
261
|
else fail_due_to("invalid token #{token.inspect}")
|
239
262
|
end
|
240
263
|
end
|
@@ -243,7 +266,7 @@ module RuboCop
|
|
243
266
|
def tokens_until(stop, what)
|
244
267
|
return to_enum __method__, stop, what unless block_given?
|
245
268
|
|
246
|
-
fail_due_to("empty #{what}") if tokens.first == stop
|
269
|
+
fail_due_to("empty #{what}") if tokens.first == stop
|
247
270
|
yield until tokens.first == stop
|
248
271
|
tokens.shift
|
249
272
|
end
|
@@ -307,11 +330,15 @@ module RuboCop
|
|
307
330
|
# @private
|
308
331
|
# Builds Ruby code for a sequence
|
309
332
|
# (head *first_terms variadic_term *last_terms)
|
310
|
-
class Sequence
|
333
|
+
class Sequence
|
334
|
+
extend Forwardable
|
335
|
+
def_delegators :@compiler, :compile_guard_clause, :with_seq_head_context,
|
336
|
+
:with_child_context, :fail_due_to
|
337
|
+
|
311
338
|
def initialize(compiler, *arity_term_list)
|
312
339
|
@arities, @terms = arity_term_list.transpose
|
313
340
|
|
314
|
-
|
341
|
+
@compiler = compiler
|
315
342
|
@variadic_index = @arities.find_index { |a| a.is_a?(Range) }
|
316
343
|
fail_due_to 'multiple variable patterns in same sequence' \
|
317
344
|
if @variadic_index && !@arities.one? { |a| a.is_a?(Range) }
|
@@ -431,7 +458,6 @@ module RuboCop
|
|
431
458
|
[0..Float::INFINITY, 'true']
|
432
459
|
end
|
433
460
|
|
434
|
-
# rubocop:disable Metrics/AbcSize
|
435
461
|
# rubocop:disable Metrics/MethodLength
|
436
462
|
def compile_any_order(capture_all = nil)
|
437
463
|
rest = capture_rest = nil
|
@@ -451,7 +477,6 @@ module RuboCop
|
|
451
477
|
end
|
452
478
|
end
|
453
479
|
# rubocop:enable Metrics/MethodLength
|
454
|
-
# rubocop:enable Metrics/AbcSize
|
455
480
|
|
456
481
|
def insure_same_captures(enum, what)
|
457
482
|
return to_enum __method__, enum, what unless block_given?
|
@@ -564,29 +589,18 @@ module RuboCop
|
|
564
589
|
end
|
565
590
|
end
|
566
591
|
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
elsif @unify.key?(name)
|
571
|
-
# we have already seen a wildcard with this name before
|
572
|
-
# so the value it matched the first time will already be stored
|
573
|
-
# in a temp. check if this value matches the one stored in the temp
|
574
|
-
"#{CUR_ELEMENT} == #{access_unify(name)}"
|
575
|
-
else
|
576
|
-
n = @unify[name] = "unify_#{name.gsub('-', '__')}"
|
577
|
-
# double assign to avoid "assigned but unused variable"
|
578
|
-
"(#{n} = #{CUR_ELEMENT}; " \
|
579
|
-
"#{n} = #{n}; true)"
|
580
|
-
end
|
581
|
-
end
|
592
|
+
# Known wildcards are considered atoms, see `compile_atom`
|
593
|
+
def compile_new_wildcard(name)
|
594
|
+
return 'true' if name.empty?
|
582
595
|
|
583
|
-
|
584
|
-
|
596
|
+
n = @unify[name] = "unify_#{name.gsub('-', '__')}"
|
597
|
+
# double assign to avoid "assigned but unused variable"
|
598
|
+
"(#{n} = #{CUR_ELEMENT}; #{n} = #{n}; true)"
|
585
599
|
end
|
586
600
|
|
587
601
|
def compile_predicate(predicate)
|
588
602
|
if predicate.end_with?('(') # is there an arglist?
|
589
|
-
args = compile_args
|
603
|
+
args = compile_args
|
590
604
|
predicate = predicate[0..-2] # drop the trailing (
|
591
605
|
"#{CUR_ELEMENT}.#{predicate}(#{args.join(',')})"
|
592
606
|
else
|
@@ -599,7 +613,7 @@ module RuboCop
|
|
599
613
|
# code is used in. pass target value as an argument
|
600
614
|
method = method[1..-1] # drop the leading #
|
601
615
|
if method.end_with?('(') # is there an arglist?
|
602
|
-
args = compile_args
|
616
|
+
args = compile_args
|
603
617
|
method = method[0..-2] # drop the trailing (
|
604
618
|
"#{method}(#{CUR_ELEMENT},#{args.join(',')})"
|
605
619
|
else
|
@@ -611,33 +625,44 @@ module RuboCop
|
|
611
625
|
"#{compile_guard_clause} && #{CUR_NODE}.#{type.tr('-', '_')}_type?"
|
612
626
|
end
|
613
627
|
|
614
|
-
def
|
615
|
-
|
628
|
+
def compile_args
|
629
|
+
tokens_until(')', 'call arguments').map do
|
630
|
+
arg = compile_arg
|
631
|
+
tokens.shift if tokens.first == ','
|
632
|
+
arg
|
633
|
+
end
|
616
634
|
end
|
617
635
|
|
618
|
-
def
|
619
|
-
|
620
|
-
|
621
|
-
tokens.slice!(0..index).each_with_object([]) do |token, args|
|
622
|
-
next if [')', ','].include?(token)
|
636
|
+
def atom_to_expr(atom)
|
637
|
+
"#{atom} === #{CUR_ELEMENT}"
|
638
|
+
end
|
623
639
|
|
624
|
-
|
640
|
+
def expr_to_atom(expr)
|
641
|
+
with_temp_variables do |compare|
|
642
|
+
in_context = with_context(expr, compare, use_temp_node: false)
|
643
|
+
"::RuboCop::AST::NodePattern::Matcher.new{|#{compare}| #{in_context}}"
|
625
644
|
end
|
626
645
|
end
|
627
646
|
|
628
|
-
|
647
|
+
# @return compiled atom (e.g. ":literal" or "SOME_CONST")
|
648
|
+
# or nil if not a simple atom (unknown wildcard, other tokens)
|
649
|
+
def compile_atom(token)
|
629
650
|
case token
|
630
|
-
when WILDCARD then
|
631
|
-
name = token[1..-1]
|
632
|
-
access_unify(name) || fail_due_to('invalid in arglist: ' + token)
|
651
|
+
when WILDCARD then access_unify(token[1..-1]) # could be nil
|
633
652
|
when LITERAL then token
|
653
|
+
when KEYWORD then get_keyword(token[1..-1])
|
654
|
+
when CONST then get_const(token[1..-1])
|
634
655
|
when PARAM then get_param(token[1..-1])
|
635
656
|
when CLOSING then fail_due_to("#{token} in invalid position")
|
636
657
|
when nil then fail_due_to('pattern ended prematurely')
|
637
|
-
else fail_due_to("invalid token in arglist: #{token.inspect}")
|
638
658
|
end
|
639
659
|
end
|
640
660
|
|
661
|
+
def compile_arg
|
662
|
+
token = tokens.shift
|
663
|
+
compile_atom(token) || expr_to_atom(compile_expr(token))
|
664
|
+
end
|
665
|
+
|
641
666
|
def next_capture
|
642
667
|
index = @captures
|
643
668
|
@captures += 1
|
@@ -650,6 +675,15 @@ module RuboCop
|
|
650
675
|
number.zero? ? @root : "param#{number}"
|
651
676
|
end
|
652
677
|
|
678
|
+
def get_keyword(name)
|
679
|
+
@keywords << name
|
680
|
+
name
|
681
|
+
end
|
682
|
+
|
683
|
+
def get_const(const)
|
684
|
+
const # Output the constant exactly as given
|
685
|
+
end
|
686
|
+
|
653
687
|
def emit_yield_capture(when_no_capture = '')
|
654
688
|
yield_val = if @captures.zero?
|
655
689
|
when_no_capture
|
@@ -675,9 +709,15 @@ module RuboCop
|
|
675
709
|
(1..@params).map { |n| "param#{n}" }.join(',')
|
676
710
|
end
|
677
711
|
|
678
|
-
def
|
712
|
+
def emit_keyword_list(forwarding: false)
|
713
|
+
pattern = "%<keyword>s: #{'%<keyword>s' if forwarding}"
|
714
|
+
@keywords.map { |k| format(pattern, keyword: k) }.join(',')
|
715
|
+
end
|
716
|
+
|
717
|
+
def emit_params(*first, forwarding: false)
|
679
718
|
params = emit_param_list
|
680
|
-
|
719
|
+
keywords = emit_keyword_list(forwarding: forwarding)
|
720
|
+
[*first, params, keywords].reject(&:empty?).join(',')
|
681
721
|
end
|
682
722
|
|
683
723
|
def emit_method_code
|
@@ -712,7 +752,7 @@ module RuboCop
|
|
712
752
|
end
|
713
753
|
|
714
754
|
def auto_use_temp_node?(code)
|
715
|
-
code.
|
755
|
+
code.match?(MULTIPLE_CUR_PLACEHOLDER)
|
716
756
|
end
|
717
757
|
|
718
758
|
# with_<...>_context methods are used whenever the context,
|
@@ -751,72 +791,59 @@ module RuboCop
|
|
751
791
|
end
|
752
792
|
|
753
793
|
def self.tokens(pattern)
|
754
|
-
pattern.scan(TOKEN).
|
794
|
+
pattern.gsub(COMMENT, '').scan(TOKEN).grep_v(ONLY_SEPARATOR)
|
755
795
|
end
|
756
|
-
end
|
757
|
-
private_constant :Compiler
|
758
|
-
|
759
|
-
# Helpers for defining methods based on a pattern string
|
760
|
-
module Macros
|
761
|
-
# Define a method which applies a pattern to an AST node
|
762
|
-
#
|
763
|
-
# The new method will return nil if the node does not match
|
764
|
-
# If the node matches, and a block is provided, the new method will
|
765
|
-
# yield to the block (passing any captures as block arguments).
|
766
|
-
# If the node matches, and no block is provided, the new method will
|
767
|
-
# return the captures, or `true` if there were none.
|
768
|
-
def def_node_matcher(method_name, pattern_str)
|
769
|
-
compiler = Compiler.new(pattern_str, 'node')
|
770
|
-
src = "def #{method_name}(node = self" \
|
771
|
-
"#{compiler.emit_trailing_params});" \
|
772
|
-
"#{compiler.emit_method_code};end"
|
773
796
|
|
774
|
-
|
775
|
-
|
797
|
+
# This method minimizes the closure for our method
|
798
|
+
def wrapping_block(method_name, **defaults)
|
799
|
+
proc do |*args, **values|
|
800
|
+
send method_name, *args, **defaults, **values
|
801
|
+
end
|
776
802
|
end
|
777
803
|
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
def def_node_search(method_name, pattern_str)
|
785
|
-
compiler = Compiler.new(pattern_str, 'node')
|
786
|
-
called_from = caller(1..1).first.split(':')
|
787
|
-
|
788
|
-
if method_name.to_s.end_with?('?')
|
789
|
-
node_search_first(method_name, compiler, called_from)
|
790
|
-
else
|
791
|
-
node_search_all(method_name, compiler, called_from)
|
804
|
+
def def_helper(base, method_name, **defaults)
|
805
|
+
location = caller_locations(3, 1).first
|
806
|
+
unless defaults.empty?
|
807
|
+
call = :"without_defaults_#{method_name}"
|
808
|
+
base.send :define_method, method_name, &wrapping_block(call, **defaults)
|
809
|
+
method_name = call
|
792
810
|
end
|
811
|
+
src = yield method_name
|
812
|
+
base.class_eval(src, location.path, location.lineno)
|
793
813
|
end
|
794
814
|
|
795
|
-
def
|
796
|
-
|
815
|
+
def def_node_matcher(base, method_name, **defaults)
|
816
|
+
def_helper(base, method_name, **defaults) do |name|
|
817
|
+
<<~RUBY
|
818
|
+
def #{name}(#{emit_params('node = self')})
|
819
|
+
#{emit_method_code}
|
820
|
+
end
|
821
|
+
RUBY
|
822
|
+
end
|
797
823
|
end
|
798
824
|
|
799
|
-
def
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
node_search(method_name, compiler, yield_code, prelude, called_from)
|
825
|
+
def def_node_search(base, method_name, **defaults)
|
826
|
+
def_helper(base, method_name, **defaults) do |name|
|
827
|
+
emit_node_search(name)
|
828
|
+
end
|
805
829
|
end
|
806
830
|
|
807
|
-
def
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
831
|
+
def emit_node_search(method_name)
|
832
|
+
if method_name.to_s.end_with?('?')
|
833
|
+
on_match = 'return true'
|
834
|
+
else
|
835
|
+
args = emit_params(":#{method_name}", @root, forwarding: true)
|
836
|
+
prelude = "return enum_for(#{args}) unless block_given?\n"
|
837
|
+
on_match = emit_yield_capture(@node_var)
|
838
|
+
end
|
839
|
+
emit_node_search_body(method_name, prelude: prelude, on_match: on_match)
|
812
840
|
end
|
813
841
|
|
814
|
-
def
|
815
|
-
on_match)
|
842
|
+
def emit_node_search_body(method_name, prelude:, on_match:)
|
816
843
|
<<~RUBY
|
817
|
-
def #{method_name}(
|
844
|
+
def #{method_name}(#{emit_params(@root)})
|
818
845
|
#{prelude}
|
819
|
-
|
846
|
+
#{@root}.each_node do |#{@node_var}|
|
820
847
|
if #{match_code}
|
821
848
|
#{on_match}
|
822
849
|
end
|
@@ -826,22 +853,53 @@ module RuboCop
|
|
826
853
|
RUBY
|
827
854
|
end
|
828
855
|
end
|
856
|
+
private_constant :Compiler
|
857
|
+
|
858
|
+
# Helpers for defining methods based on a pattern string
|
859
|
+
module Macros
|
860
|
+
# Define a method which applies a pattern to an AST node
|
861
|
+
#
|
862
|
+
# The new method will return nil if the node does not match
|
863
|
+
# If the node matches, and a block is provided, the new method will
|
864
|
+
# yield to the block (passing any captures as block arguments).
|
865
|
+
# If the node matches, and no block is provided, the new method will
|
866
|
+
# return the captures, or `true` if there were none.
|
867
|
+
def def_node_matcher(method_name, pattern_str, **keyword_defaults)
|
868
|
+
Compiler.new(pattern_str, 'node')
|
869
|
+
.def_node_matcher(self, method_name, **keyword_defaults)
|
870
|
+
end
|
871
|
+
|
872
|
+
# Define a method which recurses over the descendants of an AST node,
|
873
|
+
# checking whether any of them match the provided pattern
|
874
|
+
#
|
875
|
+
# If the method name ends with '?', the new method will return `true`
|
876
|
+
# as soon as it finds a descendant which matches. Otherwise, it will
|
877
|
+
# yield all descendants which match.
|
878
|
+
def def_node_search(method_name, pattern_str, **keyword_defaults)
|
879
|
+
Compiler.new(pattern_str, 'node0', 'node')
|
880
|
+
.def_node_search(self, method_name, **keyword_defaults)
|
881
|
+
end
|
882
|
+
end
|
829
883
|
|
830
884
|
attr_reader :pattern
|
831
885
|
|
832
886
|
def initialize(str)
|
833
887
|
@pattern = str
|
834
|
-
compiler = Compiler.new(str)
|
835
|
-
src = "def match(
|
888
|
+
compiler = Compiler.new(str, 'node0')
|
889
|
+
src = "def match(#{compiler.emit_params('node0')});" \
|
836
890
|
"#{compiler.emit_method_code}end"
|
837
891
|
instance_eval(src, __FILE__, __LINE__ + 1)
|
838
892
|
end
|
839
893
|
|
840
|
-
def match(*args)
|
894
|
+
def match(*args, **rest)
|
841
895
|
# If we're here, it's because the singleton method has not been defined,
|
842
896
|
# either because we've been dup'ed or serialized through YAML
|
843
897
|
initialize(pattern)
|
844
|
-
|
898
|
+
if rest.empty?
|
899
|
+
match(*args)
|
900
|
+
else
|
901
|
+
match(*args, **rest)
|
902
|
+
end
|
845
903
|
end
|
846
904
|
|
847
905
|
def marshal_load(pattern)
|
@@ -877,6 +935,17 @@ module RuboCop
|
|
877
935
|
|
878
936
|
nil
|
879
937
|
end
|
938
|
+
|
939
|
+
# @api private
|
940
|
+
class Matcher
|
941
|
+
def initialize(&block)
|
942
|
+
@block = block
|
943
|
+
end
|
944
|
+
|
945
|
+
def ===(compare)
|
946
|
+
@block.call(compare)
|
947
|
+
end
|
948
|
+
end
|
880
949
|
end
|
881
950
|
end
|
882
951
|
end
|