rubocop-ast 0.0.3 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|