rubocop-ast 0.3.0 → 0.8.0
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/lib/rubocop/ast.rb +21 -0
- data/lib/rubocop/ast/builder.rb +3 -0
- data/lib/rubocop/ast/ext/set.rb +12 -0
- data/lib/rubocop/ast/node.rb +96 -127
- data/lib/rubocop/ast/node/array_node.rb +1 -0
- data/lib/rubocop/ast/node/block_node.rb +2 -1
- data/lib/rubocop/ast/node/const_node.rb +65 -0
- data/lib/rubocop/ast/node/def_node.rb +5 -0
- data/lib/rubocop/ast/node/keyword_splat_node.rb +1 -0
- data/lib/rubocop/ast/node/mixin/collection_node.rb +1 -0
- data/lib/rubocop/ast/node/mixin/descendence.rb +116 -0
- data/lib/rubocop/ast/node/mixin/method_dispatch_node.rb +16 -24
- data/lib/rubocop/ast/node/mixin/method_identifier_predicates.rb +9 -0
- data/lib/rubocop/ast/node/mixin/numeric_node.rb +1 -0
- data/lib/rubocop/ast/node/mixin/parameterized_node.rb +2 -2
- data/lib/rubocop/ast/node/mixin/predicate_operator_node.rb +7 -3
- data/lib/rubocop/ast/node/pair_node.rb +4 -0
- data/lib/rubocop/ast/node/regexp_node.rb +9 -4
- 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_pattern.rb +44 -870
- data/lib/rubocop/ast/node_pattern/builder.rb +72 -0
- data/lib/rubocop/ast/node_pattern/comment.rb +45 -0
- data/lib/rubocop/ast/node_pattern/compiler.rb +104 -0
- data/lib/rubocop/ast/node_pattern/compiler/atom_subcompiler.rb +56 -0
- data/lib/rubocop/ast/node_pattern/compiler/binding.rb +78 -0
- data/lib/rubocop/ast/node_pattern/compiler/debug.rb +168 -0
- data/lib/rubocop/ast/node_pattern/compiler/node_pattern_subcompiler.rb +146 -0
- data/lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb +420 -0
- data/lib/rubocop/ast/node_pattern/compiler/subcompiler.rb +57 -0
- data/lib/rubocop/ast/node_pattern/lexer.rb +70 -0
- data/lib/rubocop/ast/node_pattern/lexer.rex +39 -0
- data/lib/rubocop/ast/node_pattern/lexer.rex.rb +182 -0
- data/lib/rubocop/ast/node_pattern/method_definer.rb +143 -0
- data/lib/rubocop/ast/node_pattern/node.rb +275 -0
- data/lib/rubocop/ast/node_pattern/parser.racc.rb +470 -0
- data/lib/rubocop/ast/node_pattern/parser.rb +66 -0
- data/lib/rubocop/ast/node_pattern/parser.y +103 -0
- data/lib/rubocop/ast/node_pattern/sets.rb +37 -0
- data/lib/rubocop/ast/node_pattern/with_meta.rb +111 -0
- data/lib/rubocop/ast/processed_source.rb +44 -3
- data/lib/rubocop/ast/rubocop_compatibility.rb +31 -0
- data/lib/rubocop/ast/traversal.rb +149 -172
- data/lib/rubocop/ast/version.rb +1 -1
- metadata +28 -4
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module AST
|
5
|
+
# Common functionality for primitive literal nodes: `sym`, `str`,
|
6
|
+
# `int`, `float`, ...
|
7
|
+
module Descendence
|
8
|
+
# Calls the given block for each child node.
|
9
|
+
# If no block is given, an `Enumerator` is returned.
|
10
|
+
#
|
11
|
+
# Note that this is different from `node.children.each { |child| ... }`
|
12
|
+
# which yields all children including non-node elements.
|
13
|
+
#
|
14
|
+
# @overload each_child_node
|
15
|
+
# Yield all nodes.
|
16
|
+
# @overload each_child_node(type)
|
17
|
+
# Yield only nodes matching the type.
|
18
|
+
# @param [Symbol] type a node type
|
19
|
+
# @overload each_child_node(type_a, type_b, ...)
|
20
|
+
# Yield only nodes matching any of the types.
|
21
|
+
# @param [Symbol] type_a a node type
|
22
|
+
# @param [Symbol] type_b a node type
|
23
|
+
# @yieldparam [Node] node each child node
|
24
|
+
# @return [self] if a block is given
|
25
|
+
# @return [Enumerator] if no block is given
|
26
|
+
def each_child_node(*types)
|
27
|
+
return to_enum(__method__, *types) unless block_given?
|
28
|
+
|
29
|
+
children.each do |child|
|
30
|
+
next unless child.is_a?(::AST::Node)
|
31
|
+
|
32
|
+
yield child if types.empty? || types.include?(child.type)
|
33
|
+
end
|
34
|
+
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns an array of child nodes.
|
39
|
+
# This is a shorthand for `node.each_child_node.to_a`.
|
40
|
+
#
|
41
|
+
# @return [Array<Node>] an array of child nodes
|
42
|
+
def child_nodes
|
43
|
+
each_child_node.to_a
|
44
|
+
end
|
45
|
+
|
46
|
+
# Calls the given block for each descendant node with depth first order.
|
47
|
+
# If no block is given, an `Enumerator` is returned.
|
48
|
+
#
|
49
|
+
# @overload each_descendant
|
50
|
+
# Yield all nodes.
|
51
|
+
# @overload each_descendant(type)
|
52
|
+
# Yield only nodes matching the type.
|
53
|
+
# @param [Symbol] type a node type
|
54
|
+
# @overload each_descendant(type_a, type_b, ...)
|
55
|
+
# Yield only nodes matching any of the types.
|
56
|
+
# @param [Symbol] type_a a node type
|
57
|
+
# @param [Symbol] type_b a node type
|
58
|
+
# @yieldparam [Node] node each descendant node
|
59
|
+
# @return [self] if a block is given
|
60
|
+
# @return [Enumerator] if no block is given
|
61
|
+
def each_descendant(*types, &block)
|
62
|
+
return to_enum(__method__, *types) unless block_given?
|
63
|
+
|
64
|
+
visit_descendants(types, &block)
|
65
|
+
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns an array of descendant nodes.
|
70
|
+
# This is a shorthand for `node.each_descendant.to_a`.
|
71
|
+
#
|
72
|
+
# @return [Array<Node>] an array of descendant nodes
|
73
|
+
def descendants
|
74
|
+
each_descendant.to_a
|
75
|
+
end
|
76
|
+
|
77
|
+
# Calls the given block for the receiver and each descendant node in
|
78
|
+
# depth-first order.
|
79
|
+
# If no block is given, an `Enumerator` is returned.
|
80
|
+
#
|
81
|
+
# This method would be useful when you treat the receiver node as the root
|
82
|
+
# of a tree and want to iterate over all nodes in the tree.
|
83
|
+
#
|
84
|
+
# @overload each_node
|
85
|
+
# Yield all nodes.
|
86
|
+
# @overload each_node(type)
|
87
|
+
# Yield only nodes matching the type.
|
88
|
+
# @param [Symbol] type a node type
|
89
|
+
# @overload each_node(type_a, type_b, ...)
|
90
|
+
# Yield only nodes matching any of the types.
|
91
|
+
# @param [Symbol] type_a a node type
|
92
|
+
# @param [Symbol] type_b a node type
|
93
|
+
# @yieldparam [Node] node each node
|
94
|
+
# @return [self] if a block is given
|
95
|
+
# @return [Enumerator] if no block is given
|
96
|
+
def each_node(*types, &block)
|
97
|
+
return to_enum(__method__, *types) unless block_given?
|
98
|
+
|
99
|
+
yield self if types.empty? || types.include?(type)
|
100
|
+
|
101
|
+
visit_descendants(types, &block)
|
102
|
+
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
protected
|
107
|
+
|
108
|
+
def visit_descendants(types, &block)
|
109
|
+
each_child_node do |child|
|
110
|
+
yield child if types.empty? || types.include?(child.type)
|
111
|
+
child.visit_descendants(types, &block)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -10,7 +10,9 @@ module RuboCop
|
|
10
10
|
include MethodIdentifierPredicates
|
11
11
|
|
12
12
|
ARITHMETIC_OPERATORS = %i[+ - * / % **].freeze
|
13
|
+
private_constant :ARITHMETIC_OPERATORS
|
13
14
|
SPECIAL_MODIFIERS = %w[private protected].freeze
|
15
|
+
private_constant :SPECIAL_MODIFIERS
|
14
16
|
|
15
17
|
# The receiving node of the method dispatch.
|
16
18
|
#
|
@@ -42,7 +44,7 @@ module RuboCop
|
|
42
44
|
#
|
43
45
|
# @return [Boolean] whether the dispatched method is a macro method
|
44
46
|
def macro?
|
45
|
-
!receiver &&
|
47
|
+
!receiver && in_macro_scope?
|
46
48
|
end
|
47
49
|
|
48
50
|
# Checks whether the dispatched method is an access modifier.
|
@@ -222,31 +224,21 @@ module RuboCop
|
|
222
224
|
|
223
225
|
private
|
224
226
|
|
225
|
-
def_node_matcher :
|
226
|
-
{
|
227
|
-
|
228
|
-
|
229
|
-
|
227
|
+
def_node_matcher :in_macro_scope?, <<~PATTERN
|
228
|
+
{
|
229
|
+
root? # Either a root node,
|
230
|
+
^{ # or the parent is...
|
231
|
+
sclass class module class_constructor? # a class-like node
|
232
|
+
[ { # or some "wrapper"
|
233
|
+
kwbegin begin block
|
234
|
+
(if _condition <%0 _>) # note: we're excluding the condition of `if` nodes
|
235
|
+
}
|
236
|
+
#in_macro_scope? # that is itself in a macro scope
|
237
|
+
]
|
238
|
+
}
|
239
|
+
}
|
230
240
|
PATTERN
|
231
241
|
|
232
|
-
# Check if a node's parent is a kwbegin wrapper within a macro scope
|
233
|
-
#
|
234
|
-
# @param parent [Node] parent of the node being checked
|
235
|
-
#
|
236
|
-
# @return [Boolean] true if the parent is a kwbegin in a macro scope
|
237
|
-
def macro_kwbegin_wrapper?(parent)
|
238
|
-
parent.kwbegin_type? && macro_scope?(parent)
|
239
|
-
end
|
240
|
-
|
241
|
-
# Check if a node does not have a parent
|
242
|
-
#
|
243
|
-
# @param node [Node]
|
244
|
-
#
|
245
|
-
# @return [Boolean] if the parent is nil
|
246
|
-
def root_node?(node)
|
247
|
-
node.parent.nil?
|
248
|
-
end
|
249
|
-
|
250
242
|
def_node_matcher :adjacent_def_modifier?, <<~PATTERN
|
251
243
|
(send nil? _ ({def defs} ...))
|
252
244
|
PATTERN
|
@@ -11,17 +11,23 @@ module RuboCop
|
|
11
11
|
find find_all find_index inject loop map!
|
12
12
|
map reduce reject reject! reverse_each select
|
13
13
|
select! times upto].to_set.freeze
|
14
|
+
private_constant :ENUMERATOR_METHODS
|
14
15
|
|
15
16
|
ENUMERABLE_METHODS = (Enumerable.instance_methods + [:each]).to_set.freeze
|
17
|
+
private_constant :ENUMERABLE_METHODS
|
16
18
|
|
17
19
|
# http://phrogz.net/programmingruby/language.html#table_18.4
|
18
20
|
OPERATOR_METHODS = %i[| ^ & <=> == === =~ > >= < <= << >> + - * /
|
19
21
|
% ** ~ +@ -@ !@ ~@ [] []= ! != !~ `].to_set.freeze
|
22
|
+
private_constant :OPERATOR_METHODS
|
20
23
|
|
21
24
|
NONMUTATING_BINARY_OPERATOR_METHODS = %i[* / % + - == === != < > <= >= <=>].to_set.freeze
|
25
|
+
private_constant :NONMUTATING_BINARY_OPERATOR_METHODS
|
22
26
|
NONMUTATING_UNARY_OPERATOR_METHODS = %i[+@ -@ ~ !].to_set.freeze
|
27
|
+
private_constant :NONMUTATING_UNARY_OPERATOR_METHODS
|
23
28
|
NONMUTATING_OPERATOR_METHODS = (NONMUTATING_BINARY_OPERATOR_METHODS +
|
24
29
|
NONMUTATING_UNARY_OPERATOR_METHODS).freeze
|
30
|
+
private_constant :NONMUTATING_OPERATOR_METHODS
|
25
31
|
|
26
32
|
NONMUTATING_ARRAY_METHODS = %i[
|
27
33
|
all? any? assoc at bsearch bsearch_index collect
|
@@ -37,6 +43,7 @@ module RuboCop
|
|
37
43
|
to_a to_ary to_h to_s transpose union uniq
|
38
44
|
values_at zip |
|
39
45
|
].to_set.freeze
|
46
|
+
private_constant :NONMUTATING_ARRAY_METHODS
|
40
47
|
|
41
48
|
NONMUTATING_HASH_METHODS = %i[
|
42
49
|
any? assoc compact dig each each_key each_pair
|
@@ -47,6 +54,7 @@ module RuboCop
|
|
47
54
|
to_proc to_s transform_keys transform_values value?
|
48
55
|
values values_at
|
49
56
|
].to_set.freeze
|
57
|
+
private_constant :NONMUTATING_HASH_METHODS
|
50
58
|
|
51
59
|
NONMUTATING_STRING_METHODS = %i[
|
52
60
|
ascii_only? b bytes bytesize byteslice capitalize
|
@@ -62,6 +70,7 @@ module RuboCop
|
|
62
70
|
to_str to_sym tr tr_s unicode_normalize unicode_normalized?
|
63
71
|
unpack unpack1 upcase upto valid_encoding?
|
64
72
|
].to_set.freeze
|
73
|
+
private_constant :NONMUTATING_STRING_METHODS
|
65
74
|
|
66
75
|
# Checks whether the method name matches the argument.
|
67
76
|
#
|
@@ -82,9 +82,9 @@ module RuboCop
|
|
82
82
|
# and optimizes other calls
|
83
83
|
module RestArguments
|
84
84
|
include ParameterizedNode
|
85
|
-
# @return [Array] arguments, if any
|
85
|
+
# @return [Array<Node>] arguments, if any
|
86
86
|
def arguments
|
87
|
-
children[first_argument_index..-1]
|
87
|
+
children[first_argument_index..-1].freeze
|
88
88
|
end
|
89
89
|
|
90
90
|
# A shorthand for getting the first argument of the node.
|
@@ -5,10 +5,14 @@ module RuboCop
|
|
5
5
|
# Common functionality for nodes that are predicates:
|
6
6
|
# `or`, `and` ...
|
7
7
|
module PredicateOperatorNode
|
8
|
-
LOGICAL_AND
|
8
|
+
LOGICAL_AND = '&&'
|
9
|
+
private_constant :LOGICAL_AND
|
9
10
|
SEMANTIC_AND = 'and'
|
10
|
-
|
11
|
-
|
11
|
+
private_constant :SEMANTIC_AND
|
12
|
+
LOGICAL_OR = '||'
|
13
|
+
private_constant :LOGICAL_OR
|
14
|
+
SEMANTIC_OR = 'or'
|
15
|
+
private_constant :SEMANTIC_OR
|
12
16
|
|
13
17
|
# Returns the operator as a string.
|
14
18
|
#
|
@@ -9,9 +9,13 @@ module RuboCop
|
|
9
9
|
include HashElementNode
|
10
10
|
|
11
11
|
HASH_ROCKET = '=>'
|
12
|
+
private_constant :HASH_ROCKET
|
12
13
|
SPACED_HASH_ROCKET = ' => '
|
14
|
+
private_constant :SPACED_HASH_ROCKET
|
13
15
|
COLON = ':'
|
16
|
+
private_constant :COLON
|
14
17
|
SPACED_COLON = ': '
|
18
|
+
private_constant :SPACED_COLON
|
15
19
|
|
16
20
|
# Checks whether the `pair` uses a hash rocket delimiter.
|
17
21
|
#
|
@@ -13,13 +13,11 @@ module RuboCop
|
|
13
13
|
n: Regexp::NOENCODING,
|
14
14
|
o: 0
|
15
15
|
}.freeze
|
16
|
+
private_constant :OPTIONS
|
16
17
|
|
17
|
-
# Note: The 'o' option is ignored.
|
18
|
-
#
|
19
18
|
# @return [Regexp] a regexp of this node
|
20
19
|
def to_regexp
|
21
|
-
|
22
|
-
Regexp.new(content, option)
|
20
|
+
Regexp.new(content, options)
|
23
21
|
end
|
24
22
|
|
25
23
|
# @return [RuboCop::AST::Node] a regopt node
|
@@ -27,6 +25,13 @@ module RuboCop
|
|
27
25
|
children.last
|
28
26
|
end
|
29
27
|
|
28
|
+
# Note: The 'o' option is ignored.
|
29
|
+
#
|
30
|
+
# @return [Integer] the Regexp option bits as returned by Regexp#options
|
31
|
+
def options
|
32
|
+
regopt.children.map { |opt| OPTIONS.fetch(opt) }.inject(0, :|)
|
33
|
+
end
|
34
|
+
|
30
35
|
# @return [String] a string of regexp content
|
31
36
|
def content
|
32
37
|
children.select(&:str_type?).map(&:str_content).join
|
@@ -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
|
@@ -1,13 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'delegate'
|
4
|
-
require 'erb'
|
5
4
|
|
6
|
-
# rubocop:disable Metrics/ClassLength, Metrics/CyclomaticComplexity
|
7
5
|
module RuboCop
|
8
6
|
module AST
|
9
7
|
# This class performs a pattern-matching operation on an AST node.
|
10
8
|
#
|
9
|
+
# Detailed syntax: /doc/modules/ROOT/pages/node_pattern.md
|
10
|
+
#
|
11
11
|
# Initialize a new `NodePattern` with `NodePattern.new(pattern_string)`, then
|
12
12
|
# pass an AST node to `NodePattern#match`. Alternatively, use one of the class
|
13
13
|
# macros in `NodePattern::Macros` to define your own pattern-matching method.
|
@@ -23,838 +23,7 @@ module RuboCop
|
|
23
23
|
# - With no block, but multiple captures: captures are returned as an array.
|
24
24
|
# - With no block and no captures: #match returns `true`.
|
25
25
|
#
|
26
|
-
# ## Pattern string format examples
|
27
|
-
#
|
28
|
-
# ':sym' # matches a literal symbol
|
29
|
-
# '1' # matches a literal integer
|
30
|
-
# 'nil' # matches a literal nil
|
31
|
-
# 'send' # matches (send ...)
|
32
|
-
# '(send)' # matches (send)
|
33
|
-
# '(send ...)' # matches (send ...)
|
34
|
-
# '(op-asgn)' # node types with hyphenated names also work
|
35
|
-
# '{send class}' # matches (send ...) or (class ...)
|
36
|
-
# '({send class})' # matches (send) or (class)
|
37
|
-
# '(send const)' # matches (send (const ...))
|
38
|
-
# '(send _ :new)' # matches (send <anything> :new)
|
39
|
-
# '(send $_ :new)' # as above, but whatever matches the $_ is captured
|
40
|
-
# '(send $_ $_)' # you can use as many captures as you want
|
41
|
-
# '(send !const ...)' # ! negates the next part of the pattern
|
42
|
-
# '$(send const ...)' # arbitrary matching can be performed on a capture
|
43
|
-
# '(send _recv _msg)' # wildcards can be named (for readability)
|
44
|
-
# '(send ... :new)' # you can match against the last children
|
45
|
-
# '(array <str sym>)' # you can match children in any order. This
|
46
|
-
# # would match `['x', :y]` as well as `[:y, 'x']
|
47
|
-
# '(_ <str sym ...>)' # will match if arguments have at least a `str` and
|
48
|
-
# # a `sym` node, but can have more.
|
49
|
-
# '(array <$str $_>)' # captures are in the order of the pattern,
|
50
|
-
# # irrespective of the actual order of the children
|
51
|
-
# '(array int*)' # will match an array of 0 or more integers
|
52
|
-
# '(array int ?)' # will match 0 or 1 integer.
|
53
|
-
# # Note: Space needed to distinguish from int?
|
54
|
-
# '(array int+)' # will match an array of 1 or more integers
|
55
|
-
# '(array (int $_)+)' # as above and will capture the numbers in an array
|
56
|
-
# '(send $...)' # capture all the children as an array
|
57
|
-
# '(send $... int)' # capture all children but the last as an array
|
58
|
-
# '(send _x :+ _x)' # unification is performed on named wildcards
|
59
|
-
# # (like Prolog variables...)
|
60
|
-
# # (#== is used to see if values unify)
|
61
|
-
# '(int odd?)' # words which end with a ? are predicate methods,
|
62
|
-
# # are are called on the target to see if it matches
|
63
|
-
# # any Ruby method which the matched object supports
|
64
|
-
# # can be used
|
65
|
-
# # if a truthy value is returned, the match succeeds
|
66
|
-
# '(int [!1 !2])' # [] contains multiple patterns, ALL of which must
|
67
|
-
# # match in that position
|
68
|
-
# # in other words, while {} is pattern union (logical
|
69
|
-
# # OR), [] is intersection (logical AND)
|
70
|
-
# '(send %1 _)' # % stands for a parameter which must be supplied to
|
71
|
-
# # #match at matching time
|
72
|
-
# # it will be compared to the corresponding value in
|
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.
|
79
|
-
# # a bare '%' is the same as '%1'
|
80
|
-
# # the number of extra parameters passed to #match
|
81
|
-
# # must equal the highest % value in the pattern
|
82
|
-
# # for consistency, %0 is the 'root node' which is
|
83
|
-
# # passed as the 1st argument to #match, where the
|
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`.
|
90
|
-
# '^^send' # each ^ ascends one level in the AST
|
91
|
-
# # so this matches against the grandparent node
|
92
|
-
# '`send' # descends any number of level in the AST
|
93
|
-
# # so this matches against any descendant node
|
94
|
-
# '#method' # we call this a 'funcall'; it calls a method in the
|
95
|
-
# # context where a pattern-matching method is defined
|
96
|
-
# # if that returns a truthy value, the match succeeds
|
97
|
-
# 'equal?(%1)' # predicates can be given 1 or more extra args
|
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
|
103
|
-
#
|
104
|
-
# You can nest arbitrarily deep:
|
105
|
-
#
|
106
|
-
# # matches node parsed from 'Const = Class.new' or 'Const = Module.new':
|
107
|
-
# '(casgn nil? :Const (send (const nil? {:Class :Module}) :new))'
|
108
|
-
# # matches a node parsed from an 'if', with a '==' comparison,
|
109
|
-
# # and no 'else' branch:
|
110
|
-
# '(if (send _ :== _) _ nil?)'
|
111
|
-
#
|
112
|
-
# Note that patterns like 'send' are implemented by calling `#send_type?` on
|
113
|
-
# the node being matched, 'const' by `#const_type?`, 'int' by `#int_type?`,
|
114
|
-
# and so on. Therefore, if you add methods which are named like
|
115
|
-
# `#prefix_type?` to the AST node class, then 'prefix' will become usable as
|
116
|
-
# a pattern.
|
117
26
|
class NodePattern
|
118
|
-
# @private
|
119
|
-
Invalid = Class.new(StandardError)
|
120
|
-
|
121
|
-
# @private
|
122
|
-
# Builds Ruby code which implements a pattern
|
123
|
-
class Compiler
|
124
|
-
SYMBOL = %r{:(?:[\w+@*/?!<>=~|%^-]+|\[\]=?)}.freeze
|
125
|
-
IDENTIFIER = /[a-zA-Z_][a-zA-Z0-9_-]*/.freeze
|
126
|
-
COMMENT = /#\s.*$/.freeze
|
127
|
-
|
128
|
-
META = Regexp.union(
|
129
|
-
%w"( ) { } [ ] $< < > $... $ ! ^ ` ... + * ?"
|
130
|
-
).freeze
|
131
|
-
NUMBER = /-?\d+(?:\.\d+)?/.freeze
|
132
|
-
STRING = /".+?"/.freeze
|
133
|
-
METHOD_NAME = /\#?#{IDENTIFIER}[!?]?\(?/.freeze
|
134
|
-
PARAM_CONST = /%[A-Z:][a-zA-Z_:]+/.freeze
|
135
|
-
KEYWORD_NAME = /%[a-z_]+/.freeze
|
136
|
-
PARAM_NUMBER = /%\d*/.freeze
|
137
|
-
|
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)
|
143
|
-
|
144
|
-
TOKEN = /\G(?:#{SEPARATORS}|#{TOKENS}|.)/.freeze
|
145
|
-
|
146
|
-
NODE = /\A#{IDENTIFIER}\Z/.freeze
|
147
|
-
PREDICATE = /\A#{IDENTIFIER}\?\(?\Z/.freeze
|
148
|
-
WILDCARD = /\A_(?:#{IDENTIFIER})?\Z/.freeze
|
149
|
-
|
150
|
-
FUNCALL = /\A\##{METHOD_NAME}/.freeze
|
151
|
-
LITERAL = /\A(?:#{SYMBOL}|#{NUMBER}|#{STRING})\Z/.freeze
|
152
|
-
PARAM = /\A#{PARAM_NUMBER}\Z/.freeze
|
153
|
-
CONST = /\A#{PARAM_CONST}\Z/.freeze
|
154
|
-
KEYWORD = /\A#{KEYWORD_NAME}\Z/.freeze
|
155
|
-
CLOSING = /\A(?:\)|\}|\])\Z/.freeze
|
156
|
-
|
157
|
-
REST = '...'
|
158
|
-
CAPTURED_REST = '$...'
|
159
|
-
|
160
|
-
attr_reader :match_code, :tokens, :captures
|
161
|
-
|
162
|
-
SEQ_HEAD_INDEX = -1
|
163
|
-
|
164
|
-
# Placeholders while compiling, see with_..._context methods
|
165
|
-
CUR_PLACEHOLDER = '@@@cur'
|
166
|
-
CUR_NODE = "#{CUR_PLACEHOLDER} node@@@"
|
167
|
-
CUR_ELEMENT = "#{CUR_PLACEHOLDER} element@@@"
|
168
|
-
SEQ_HEAD_GUARD = '@@@seq guard head@@@'
|
169
|
-
MULTIPLE_CUR_PLACEHOLDER = /#{CUR_PLACEHOLDER}.*#{CUR_PLACEHOLDER}/.freeze
|
170
|
-
|
171
|
-
line = __LINE__
|
172
|
-
ANY_ORDER_TEMPLATE = ERB.new <<~RUBY.gsub("-%>\n", '%>')
|
173
|
-
<% if capture_rest %>(<%= capture_rest %> = []) && <% end -%>
|
174
|
-
<% if capture_all %>(<%= capture_all %> = <% end -%>
|
175
|
-
<%= CUR_NODE %>.children[<%= range %>]<% if capture_all %>)<% end -%>
|
176
|
-
.each_with_object({}) { |<%= child %>, <%= matched %>|
|
177
|
-
case
|
178
|
-
<% patterns.each_with_index do |pattern, i| -%>
|
179
|
-
when !<%= matched %>[<%= i %>] && <%=
|
180
|
-
with_context(pattern, child, use_temp_node: false)
|
181
|
-
%> then <%= matched %>[<%= i %>] = true
|
182
|
-
<% end -%>
|
183
|
-
<% if !rest %> else break({})
|
184
|
-
<% elsif capture_rest %> else <%= capture_rest %> << <%= child %>
|
185
|
-
<% end -%>
|
186
|
-
end
|
187
|
-
}.size == <%= patterns.size -%>
|
188
|
-
RUBY
|
189
|
-
ANY_ORDER_TEMPLATE.location = [__FILE__, line + 1]
|
190
|
-
|
191
|
-
line = __LINE__
|
192
|
-
REPEATED_TEMPLATE = ERB.new <<~RUBY.gsub("-%>\n", '%>')
|
193
|
-
<% if captured %>(<%= accumulate %> = Array.new) && <% end %>
|
194
|
-
<%= CUR_NODE %>.children[<%= range %>].all? do |<%= child %>|
|
195
|
-
<%= with_context(expr, child, use_temp_node: false) %><% if captured %>&&
|
196
|
-
<%= accumulate %>.push(<%= captured %>)<% end %>
|
197
|
-
end <% if captured %>&&
|
198
|
-
(<%= captured %> = if <%= accumulate %>.empty?
|
199
|
-
<%= captured %>.map{[]} # Transpose hack won't work for empty case
|
200
|
-
else
|
201
|
-
<%= accumulate %>.transpose
|
202
|
-
end) <% end -%>
|
203
|
-
RUBY
|
204
|
-
REPEATED_TEMPLATE.location = [__FILE__, line + 1]
|
205
|
-
|
206
|
-
def initialize(str, root = 'node0', node_var = root)
|
207
|
-
@string = str
|
208
|
-
# For def_node_pattern, 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
|
213
|
-
|
214
|
-
@temps = 0 # avoid name clashes between temp variables
|
215
|
-
@captures = 0 # number of captures seen
|
216
|
-
@unify = {} # named wildcard -> temp variable
|
217
|
-
@params = 0 # highest % (param) number seen
|
218
|
-
@keywords = Set[] # keyword parameters seen
|
219
|
-
run
|
220
|
-
end
|
221
|
-
|
222
|
-
def run
|
223
|
-
@tokens = Compiler.tokens(@string)
|
224
|
-
|
225
|
-
@match_code = with_context(compile_expr, @node_var, use_temp_node: false)
|
226
|
-
@match_code.prepend("(captures = Array.new(#{@captures})) && ") \
|
227
|
-
if @captures.positive?
|
228
|
-
|
229
|
-
fail_due_to('unbalanced pattern') unless tokens.empty?
|
230
|
-
end
|
231
|
-
|
232
|
-
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
233
|
-
def compile_expr(token = tokens.shift)
|
234
|
-
# read a single pattern-matching expression from the token stream,
|
235
|
-
# return Ruby code which performs the corresponding matching operation
|
236
|
-
#
|
237
|
-
# the 'pattern-matching' expression may be a composite which
|
238
|
-
# contains an arbitrary number of sub-expressions, but that composite
|
239
|
-
# must all have precedence higher or equal to that of `&&`
|
240
|
-
#
|
241
|
-
# Expressions may use placeholders like:
|
242
|
-
# CUR_NODE: Ruby code that evaluates to an AST node
|
243
|
-
# CUR_ELEMENT: Either the node or the type if in first element of
|
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
|
-
|
249
|
-
case token
|
250
|
-
when '(' then compile_seq
|
251
|
-
when '{' then compile_union
|
252
|
-
when '[' then compile_intersect
|
253
|
-
when '!' then compile_negation
|
254
|
-
when '$' then compile_capture
|
255
|
-
when '^' then compile_ascend
|
256
|
-
when '`' then compile_descend
|
257
|
-
when WILDCARD then compile_new_wildcard(token[1..-1])
|
258
|
-
when FUNCALL then compile_funcall(token)
|
259
|
-
when PREDICATE then compile_predicate(token)
|
260
|
-
when NODE then compile_nodetype(token)
|
261
|
-
else fail_due_to("invalid token #{token.inspect}")
|
262
|
-
end
|
263
|
-
end
|
264
|
-
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
265
|
-
|
266
|
-
def tokens_until(stop, what)
|
267
|
-
return to_enum __method__, stop, what unless block_given?
|
268
|
-
|
269
|
-
fail_due_to("empty #{what}") if tokens.first == stop
|
270
|
-
yield until tokens.first == stop
|
271
|
-
tokens.shift
|
272
|
-
end
|
273
|
-
|
274
|
-
def compile_seq
|
275
|
-
terms = tokens_until(')', 'sequence').map { variadic_seq_term }
|
276
|
-
Sequence.new(self, *terms).compile
|
277
|
-
end
|
278
|
-
|
279
|
-
def compile_guard_clause
|
280
|
-
"#{CUR_NODE}.is_a?(RuboCop::AST::Node)"
|
281
|
-
end
|
282
|
-
|
283
|
-
def variadic_seq_term
|
284
|
-
token = tokens.shift
|
285
|
-
case token
|
286
|
-
when CAPTURED_REST then compile_captured_ellipsis
|
287
|
-
when REST then compile_ellipsis
|
288
|
-
when '$<' then compile_any_order(next_capture)
|
289
|
-
when '<' then compile_any_order
|
290
|
-
else compile_repeated_expr(token)
|
291
|
-
end
|
292
|
-
end
|
293
|
-
|
294
|
-
def compile_repeated_expr(token)
|
295
|
-
before = @captures
|
296
|
-
expr = compile_expr(token)
|
297
|
-
min, max = parse_repetition_token
|
298
|
-
return [1, expr] if min.nil?
|
299
|
-
|
300
|
-
if @captures != before
|
301
|
-
captured = "captures[#{before}...#{@captures}]"
|
302
|
-
accumulate = next_temp_variable(:accumulate)
|
303
|
-
end
|
304
|
-
arity = min..max || Float::INFINITY
|
305
|
-
|
306
|
-
[arity, repeated_generator(expr, captured, accumulate)]
|
307
|
-
end
|
308
|
-
|
309
|
-
def repeated_generator(expr, captured, accumulate)
|
310
|
-
with_temp_variables do |child|
|
311
|
-
lambda do |range|
|
312
|
-
fail_due_to 'repeated pattern at beginning of sequence' if range.begin == SEQ_HEAD_INDEX
|
313
|
-
REPEATED_TEMPLATE.result(binding)
|
314
|
-
end
|
315
|
-
end
|
316
|
-
end
|
317
|
-
|
318
|
-
def parse_repetition_token
|
319
|
-
case tokens.first
|
320
|
-
when '*' then min = 0
|
321
|
-
when '+' then min = 1
|
322
|
-
when '?' then min = 0
|
323
|
-
max = 1
|
324
|
-
else return
|
325
|
-
end
|
326
|
-
tokens.shift
|
327
|
-
[min, max]
|
328
|
-
end
|
329
|
-
|
330
|
-
# @private
|
331
|
-
# Builds Ruby code for a sequence
|
332
|
-
# (head *first_terms variadic_term *last_terms)
|
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
|
-
|
338
|
-
def initialize(compiler, *arity_term_list)
|
339
|
-
@arities, @terms = arity_term_list.transpose
|
340
|
-
|
341
|
-
@compiler = compiler
|
342
|
-
@variadic_index = @arities.find_index { |a| a.is_a?(Range) }
|
343
|
-
fail_due_to 'multiple variable patterns in same sequence' \
|
344
|
-
if @variadic_index && !@arities.one? { |a| a.is_a?(Range) }
|
345
|
-
end
|
346
|
-
|
347
|
-
def compile
|
348
|
-
[
|
349
|
-
compile_guard_clause,
|
350
|
-
compile_child_nb_guard,
|
351
|
-
compile_seq_head,
|
352
|
-
*compile_first_terms,
|
353
|
-
compile_variadic_term,
|
354
|
-
*compile_last_terms
|
355
|
-
].compact.join(" &&\n") << SEQ_HEAD_GUARD
|
356
|
-
end
|
357
|
-
|
358
|
-
private
|
359
|
-
|
360
|
-
def first_terms_arity
|
361
|
-
first_terms_range { |r| @arities[r].inject(0, :+) } || 0
|
362
|
-
end
|
363
|
-
|
364
|
-
def last_terms_arity
|
365
|
-
last_terms_range { |r| @arities[r].inject(0, :+) } || 0
|
366
|
-
end
|
367
|
-
|
368
|
-
def variadic_term_min_arity
|
369
|
-
@variadic_index ? @arities[@variadic_index].begin : 0
|
370
|
-
end
|
371
|
-
|
372
|
-
def first_terms_range
|
373
|
-
yield 1..(@variadic_index || @terms.size) - 1 if seq_head?
|
374
|
-
end
|
375
|
-
|
376
|
-
def last_terms_range
|
377
|
-
yield @variadic_index + 1...@terms.size if @variadic_index
|
378
|
-
end
|
379
|
-
|
380
|
-
def seq_head?
|
381
|
-
@variadic_index != 0
|
382
|
-
end
|
383
|
-
|
384
|
-
def compile_child_nb_guard
|
385
|
-
fixed = first_terms_arity + last_terms_arity
|
386
|
-
min = fixed + variadic_term_min_arity
|
387
|
-
op = if @variadic_index
|
388
|
-
max_variadic = @arities[@variadic_index].end
|
389
|
-
if max_variadic != Float::INFINITY
|
390
|
-
range = min..fixed + max_variadic
|
391
|
-
return "(#{range}).cover?(#{CUR_NODE}.children.size)"
|
392
|
-
end
|
393
|
-
'>='
|
394
|
-
else
|
395
|
-
'=='
|
396
|
-
end
|
397
|
-
"#{CUR_NODE}.children.size #{op} #{min}"
|
398
|
-
end
|
399
|
-
|
400
|
-
def term(index, range)
|
401
|
-
t = @terms[index]
|
402
|
-
if t.respond_to? :call
|
403
|
-
t.call(range)
|
404
|
-
else
|
405
|
-
with_child_context(t, range.begin)
|
406
|
-
end
|
407
|
-
end
|
408
|
-
|
409
|
-
def compile_seq_head
|
410
|
-
return unless seq_head?
|
411
|
-
|
412
|
-
fail_due_to 'sequences cannot start with <' \
|
413
|
-
if @terms[0].respond_to? :call
|
414
|
-
|
415
|
-
with_seq_head_context(@terms[0])
|
416
|
-
end
|
417
|
-
|
418
|
-
def compile_first_terms
|
419
|
-
first_terms_range { |range| compile_terms(range, 0) }
|
420
|
-
end
|
421
|
-
|
422
|
-
def compile_last_terms
|
423
|
-
last_terms_range { |r| compile_terms(r, -last_terms_arity) }
|
424
|
-
end
|
425
|
-
|
426
|
-
def compile_terms(index_range, start)
|
427
|
-
index_range.map do |i|
|
428
|
-
current = start
|
429
|
-
start += @arities.fetch(i)
|
430
|
-
term(i, current..start - 1)
|
431
|
-
end
|
432
|
-
end
|
433
|
-
|
434
|
-
def compile_variadic_term
|
435
|
-
variadic_arity { |arity| term(@variadic_index, arity) }
|
436
|
-
end
|
437
|
-
|
438
|
-
def variadic_arity
|
439
|
-
return unless @variadic_index
|
440
|
-
|
441
|
-
first = @variadic_index.positive? ? first_terms_arity : SEQ_HEAD_INDEX
|
442
|
-
yield first..-last_terms_arity - 1
|
443
|
-
end
|
444
|
-
end
|
445
|
-
private_constant :Sequence
|
446
|
-
|
447
|
-
def compile_captured_ellipsis
|
448
|
-
capture = next_capture
|
449
|
-
block = lambda { |range|
|
450
|
-
# Consider ($...) like (_ $...):
|
451
|
-
range = 0..range.end if range.begin == SEQ_HEAD_INDEX
|
452
|
-
"(#{capture} = #{CUR_NODE}.children[#{range}])"
|
453
|
-
}
|
454
|
-
[0..Float::INFINITY, block]
|
455
|
-
end
|
456
|
-
|
457
|
-
def compile_ellipsis
|
458
|
-
[0..Float::INFINITY, 'true']
|
459
|
-
end
|
460
|
-
|
461
|
-
# rubocop:disable Metrics/MethodLength
|
462
|
-
def compile_any_order(capture_all = nil)
|
463
|
-
rest = capture_rest = nil
|
464
|
-
patterns = []
|
465
|
-
with_temp_variables do |child, matched|
|
466
|
-
tokens_until('>', 'any child') do
|
467
|
-
fail_due_to 'ellipsis must be at the end of <>' if rest
|
468
|
-
token = tokens.shift
|
469
|
-
case token
|
470
|
-
when CAPTURED_REST then rest = capture_rest = next_capture
|
471
|
-
when REST then rest = true
|
472
|
-
else patterns << compile_expr(token)
|
473
|
-
end
|
474
|
-
end
|
475
|
-
[rest ? patterns.size..Float::INFINITY : patterns.size,
|
476
|
-
->(range) { ANY_ORDER_TEMPLATE.result(binding) }]
|
477
|
-
end
|
478
|
-
end
|
479
|
-
# rubocop:enable Metrics/MethodLength
|
480
|
-
|
481
|
-
def insure_same_captures(enum, what)
|
482
|
-
return to_enum __method__, enum, what unless block_given?
|
483
|
-
|
484
|
-
captures_before = captures_after = nil
|
485
|
-
enum.each do
|
486
|
-
captures_before ||= @captures
|
487
|
-
@captures = captures_before
|
488
|
-
yield
|
489
|
-
captures_after ||= @captures
|
490
|
-
fail_due_to("each #{what} must have same # of captures") if captures_after != @captures
|
491
|
-
end
|
492
|
-
end
|
493
|
-
|
494
|
-
def access_unify(name)
|
495
|
-
var = @unify[name]
|
496
|
-
|
497
|
-
if var == :forbidden_unification
|
498
|
-
fail_due_to "Wildcard #{name} was first seen in a subset of a" \
|
499
|
-
" union and can't be used outside that union"
|
500
|
-
end
|
501
|
-
var
|
502
|
-
end
|
503
|
-
|
504
|
-
def forbid_unification(*names)
|
505
|
-
names.each do |name|
|
506
|
-
@unify[name] = :forbidden_unification
|
507
|
-
end
|
508
|
-
end
|
509
|
-
|
510
|
-
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
511
|
-
def unify_in_union(enum)
|
512
|
-
# We need to reset @unify before each branch is processed.
|
513
|
-
# Moreover we need to keep track of newly encountered wildcards.
|
514
|
-
# Var `new_unify_intersection` will hold those that are encountered
|
515
|
-
# in all branches; these are not a problem.
|
516
|
-
# Var `partial_unify` will hold those encountered in only a subset
|
517
|
-
# of the branches; these can't be used outside of the union.
|
518
|
-
|
519
|
-
return to_enum __method__, enum unless block_given?
|
520
|
-
|
521
|
-
new_unify_intersection = nil
|
522
|
-
partial_unify = []
|
523
|
-
unify_before = @unify.dup
|
524
|
-
|
525
|
-
result = enum.each do |e|
|
526
|
-
@unify = unify_before.dup if new_unify_intersection
|
527
|
-
yield e
|
528
|
-
new_unify = @unify.keys - unify_before.keys
|
529
|
-
if new_unify_intersection.nil?
|
530
|
-
# First iteration
|
531
|
-
new_unify_intersection = new_unify
|
532
|
-
else
|
533
|
-
union = new_unify_intersection | new_unify
|
534
|
-
new_unify_intersection &= new_unify
|
535
|
-
partial_unify |= union - new_unify_intersection
|
536
|
-
end
|
537
|
-
end
|
538
|
-
|
539
|
-
# At this point, all members of `new_unify_intersection` can be used
|
540
|
-
# for unification outside of the union, but partial_unify may not
|
541
|
-
|
542
|
-
forbid_unification(*partial_unify)
|
543
|
-
|
544
|
-
result
|
545
|
-
end
|
546
|
-
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
547
|
-
|
548
|
-
def compile_union
|
549
|
-
# we need to ensure that each branch of the {} contains the same
|
550
|
-
# number of captures (since only one branch of the {} can actually
|
551
|
-
# match, the same variables are used to hold the captures for each
|
552
|
-
# branch)
|
553
|
-
enum = tokens_until('}', 'union')
|
554
|
-
enum = unify_in_union(enum)
|
555
|
-
terms = insure_same_captures(enum, 'branch of {}')
|
556
|
-
.map { compile_expr }
|
557
|
-
|
558
|
-
"(#{terms.join(' || ')})"
|
559
|
-
end
|
560
|
-
|
561
|
-
def compile_intersect
|
562
|
-
tokens_until(']', 'intersection')
|
563
|
-
.map { compile_expr }
|
564
|
-
.join(' && ')
|
565
|
-
end
|
566
|
-
|
567
|
-
def compile_capture
|
568
|
-
"(#{next_capture} = #{CUR_ELEMENT}; #{compile_expr})"
|
569
|
-
end
|
570
|
-
|
571
|
-
def compile_negation
|
572
|
-
"!(#{compile_expr})"
|
573
|
-
end
|
574
|
-
|
575
|
-
def compile_ascend
|
576
|
-
with_context("#{CUR_NODE} && #{compile_expr}", "#{CUR_NODE}.parent")
|
577
|
-
end
|
578
|
-
|
579
|
-
def compile_descend
|
580
|
-
with_temp_variables do |descendant|
|
581
|
-
pattern = with_context(compile_expr, descendant,
|
582
|
-
use_temp_node: false)
|
583
|
-
[
|
584
|
-
"RuboCop::AST::NodePattern.descend(#{CUR_ELEMENT}).",
|
585
|
-
"any? do |#{descendant}|",
|
586
|
-
" #{pattern}",
|
587
|
-
'end'
|
588
|
-
].join("\n")
|
589
|
-
end
|
590
|
-
end
|
591
|
-
|
592
|
-
# Known wildcards are considered atoms, see `compile_atom`
|
593
|
-
def compile_new_wildcard(name)
|
594
|
-
return 'true' if name.empty?
|
595
|
-
|
596
|
-
n = @unify[name] = "unify_#{name.gsub('-', '__')}"
|
597
|
-
# double assign to avoid "assigned but unused variable"
|
598
|
-
"(#{n} = #{CUR_ELEMENT}; #{n} = #{n}; true)"
|
599
|
-
end
|
600
|
-
|
601
|
-
def compile_predicate(predicate)
|
602
|
-
if predicate.end_with?('(') # is there an arglist?
|
603
|
-
args = compile_args
|
604
|
-
predicate = predicate[0..-2] # drop the trailing (
|
605
|
-
"#{CUR_ELEMENT}.#{predicate}(#{args.join(',')})"
|
606
|
-
else
|
607
|
-
"#{CUR_ELEMENT}.#{predicate}"
|
608
|
-
end
|
609
|
-
end
|
610
|
-
|
611
|
-
def compile_funcall(method)
|
612
|
-
# call a method in the context which this pattern-matching
|
613
|
-
# code is used in. pass target value as an argument
|
614
|
-
method = method[1..-1] # drop the leading #
|
615
|
-
if method.end_with?('(') # is there an arglist?
|
616
|
-
args = compile_args
|
617
|
-
method = method[0..-2] # drop the trailing (
|
618
|
-
"#{method}(#{CUR_ELEMENT},#{args.join(',')})"
|
619
|
-
else
|
620
|
-
"#{method}(#{CUR_ELEMENT})"
|
621
|
-
end
|
622
|
-
end
|
623
|
-
|
624
|
-
def compile_nodetype(type)
|
625
|
-
"#{compile_guard_clause} && #{CUR_NODE}.#{type.tr('-', '_')}_type?"
|
626
|
-
end
|
627
|
-
|
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
|
634
|
-
end
|
635
|
-
|
636
|
-
def atom_to_expr(atom)
|
637
|
-
"#{atom} === #{CUR_ELEMENT}"
|
638
|
-
end
|
639
|
-
|
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}}"
|
644
|
-
end
|
645
|
-
end
|
646
|
-
|
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)
|
650
|
-
case token
|
651
|
-
when WILDCARD then access_unify(token[1..-1]) # could be nil
|
652
|
-
when LITERAL then token
|
653
|
-
when KEYWORD then get_keyword(token[1..-1])
|
654
|
-
when CONST then get_const(token[1..-1])
|
655
|
-
when PARAM then get_param(token[1..-1])
|
656
|
-
when CLOSING then fail_due_to("#{token} in invalid position")
|
657
|
-
when nil then fail_due_to('pattern ended prematurely')
|
658
|
-
end
|
659
|
-
end
|
660
|
-
|
661
|
-
def compile_arg
|
662
|
-
token = tokens.shift
|
663
|
-
compile_atom(token) || expr_to_atom(compile_expr(token))
|
664
|
-
end
|
665
|
-
|
666
|
-
def next_capture
|
667
|
-
index = @captures
|
668
|
-
@captures += 1
|
669
|
-
"captures[#{index}]"
|
670
|
-
end
|
671
|
-
|
672
|
-
def get_param(number)
|
673
|
-
number = number.empty? ? 1 : Integer(number)
|
674
|
-
@params = number if number > @params
|
675
|
-
number.zero? ? @root : "param#{number}"
|
676
|
-
end
|
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
|
-
|
687
|
-
def emit_yield_capture(when_no_capture = '')
|
688
|
-
yield_val = if @captures.zero?
|
689
|
-
when_no_capture
|
690
|
-
elsif @captures == 1
|
691
|
-
'captures[0]' # Circumvent https://github.com/jruby/jruby/issues/5710
|
692
|
-
else
|
693
|
-
'*captures'
|
694
|
-
end
|
695
|
-
"yield(#{yield_val})"
|
696
|
-
end
|
697
|
-
|
698
|
-
def emit_retval
|
699
|
-
if @captures.zero?
|
700
|
-
'true'
|
701
|
-
elsif @captures == 1
|
702
|
-
'captures[0]'
|
703
|
-
else
|
704
|
-
'captures'
|
705
|
-
end
|
706
|
-
end
|
707
|
-
|
708
|
-
def emit_param_list
|
709
|
-
(1..@params).map { |n| "param#{n}" }.join(',')
|
710
|
-
end
|
711
|
-
|
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)
|
718
|
-
params = emit_param_list
|
719
|
-
keywords = emit_keyword_list(forwarding: forwarding)
|
720
|
-
[*first, params, keywords].reject(&:empty?).join(',')
|
721
|
-
end
|
722
|
-
|
723
|
-
def emit_method_code
|
724
|
-
<<~RUBY
|
725
|
-
return unless #{@match_code}
|
726
|
-
block_given? ? #{emit_yield_capture} : (return #{emit_retval})
|
727
|
-
RUBY
|
728
|
-
end
|
729
|
-
|
730
|
-
def fail_due_to(message)
|
731
|
-
raise Invalid, "Couldn't compile due to #{message}. Pattern: #{@string}"
|
732
|
-
end
|
733
|
-
|
734
|
-
def with_temp_node(cur_node)
|
735
|
-
with_temp_variables do |node|
|
736
|
-
yield "(#{node} = #{cur_node})", node
|
737
|
-
end
|
738
|
-
.gsub("\n", "\n ") # Nicer indent for debugging
|
739
|
-
end
|
740
|
-
|
741
|
-
def with_temp_variables(&block)
|
742
|
-
names = block.parameters.map { |_, name| next_temp_variable(name) }
|
743
|
-
yield(*names)
|
744
|
-
end
|
745
|
-
|
746
|
-
def next_temp_variable(name)
|
747
|
-
"#{name}#{next_temp_value}"
|
748
|
-
end
|
749
|
-
|
750
|
-
def next_temp_value
|
751
|
-
@temps += 1
|
752
|
-
end
|
753
|
-
|
754
|
-
def auto_use_temp_node?(code)
|
755
|
-
code.match?(MULTIPLE_CUR_PLACEHOLDER)
|
756
|
-
end
|
757
|
-
|
758
|
-
# with_<...>_context methods are used whenever the context,
|
759
|
-
# i.e the current node or the current element can be determined.
|
760
|
-
|
761
|
-
def with_child_context(code, child_index)
|
762
|
-
with_context(code, "#{CUR_NODE}.children[#{child_index}]")
|
763
|
-
end
|
764
|
-
|
765
|
-
def with_context(code, cur_node,
|
766
|
-
use_temp_node: auto_use_temp_node?(code))
|
767
|
-
if use_temp_node
|
768
|
-
with_temp_node(cur_node) do |init, temp_var|
|
769
|
-
substitute_cur_node(code, temp_var, first_cur_node: init)
|
770
|
-
end
|
771
|
-
else
|
772
|
-
substitute_cur_node(code, cur_node)
|
773
|
-
end
|
774
|
-
end
|
775
|
-
|
776
|
-
def with_seq_head_context(code)
|
777
|
-
fail_due_to('parentheses at sequence head') if code.include?(SEQ_HEAD_GUARD)
|
778
|
-
|
779
|
-
code.gsub CUR_ELEMENT, "#{CUR_NODE}.type"
|
780
|
-
end
|
781
|
-
|
782
|
-
def substitute_cur_node(code, cur_node, first_cur_node: cur_node)
|
783
|
-
iter = 0
|
784
|
-
code
|
785
|
-
.gsub(CUR_ELEMENT, CUR_NODE)
|
786
|
-
.gsub(CUR_NODE) do
|
787
|
-
iter += 1
|
788
|
-
iter == 1 ? first_cur_node : cur_node
|
789
|
-
end
|
790
|
-
.gsub(SEQ_HEAD_GUARD, '')
|
791
|
-
end
|
792
|
-
|
793
|
-
def self.tokens(pattern)
|
794
|
-
pattern.gsub(COMMENT, '').scan(TOKEN).grep_v(ONLY_SEPARATOR)
|
795
|
-
end
|
796
|
-
|
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
|
802
|
-
end
|
803
|
-
|
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
|
810
|
-
end
|
811
|
-
src = yield method_name
|
812
|
-
base.class_eval(src, location.path, location.lineno)
|
813
|
-
end
|
814
|
-
|
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
|
823
|
-
end
|
824
|
-
|
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
|
829
|
-
end
|
830
|
-
|
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)
|
840
|
-
end
|
841
|
-
|
842
|
-
def emit_node_search_body(method_name, prelude:, on_match:)
|
843
|
-
<<~RUBY
|
844
|
-
def #{method_name}(#{emit_params(@root)})
|
845
|
-
#{prelude}
|
846
|
-
#{@root}.each_node do |#{@node_var}|
|
847
|
-
if #{match_code}
|
848
|
-
#{on_match}
|
849
|
-
end
|
850
|
-
end
|
851
|
-
nil
|
852
|
-
end
|
853
|
-
RUBY
|
854
|
-
end
|
855
|
-
end
|
856
|
-
private_constant :Compiler
|
857
|
-
|
858
27
|
# Helpers for defining methods based on a pattern string
|
859
28
|
module Macros
|
860
29
|
# Define a method which applies a pattern to an AST node
|
@@ -865,8 +34,7 @@ module RuboCop
|
|
865
34
|
# If the node matches, and no block is provided, the new method will
|
866
35
|
# return the captures, or `true` if there were none.
|
867
36
|
def def_node_matcher(method_name, pattern_str, **keyword_defaults)
|
868
|
-
|
869
|
-
.def_node_matcher(self, method_name, **keyword_defaults)
|
37
|
+
NodePattern.new(pattern_str).def_node_matcher(self, method_name, **keyword_defaults)
|
870
38
|
end
|
871
39
|
|
872
40
|
# Define a method which recurses over the descendants of an AST node,
|
@@ -876,48 +44,60 @@ module RuboCop
|
|
876
44
|
# as soon as it finds a descendant which matches. Otherwise, it will
|
877
45
|
# yield all descendants which match.
|
878
46
|
def def_node_search(method_name, pattern_str, **keyword_defaults)
|
879
|
-
|
880
|
-
.def_node_search(self, method_name, **keyword_defaults)
|
47
|
+
NodePattern.new(pattern_str).def_node_search(self, method_name, **keyword_defaults)
|
881
48
|
end
|
882
49
|
end
|
883
50
|
|
884
|
-
|
51
|
+
extend Forwardable
|
52
|
+
include MethodDefiner
|
53
|
+
Invalid = Class.new(StandardError)
|
885
54
|
|
886
|
-
|
55
|
+
VAR = 'node'
|
56
|
+
|
57
|
+
attr_reader :pattern, :ast, :match_code
|
58
|
+
|
59
|
+
def_delegators :@compiler, :captures, :named_parameters, :positional_parameters
|
60
|
+
|
61
|
+
def initialize(str, compiler: Compiler.new)
|
887
62
|
@pattern = str
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
63
|
+
@ast = compiler.parser.parse(str)
|
64
|
+
@compiler = compiler
|
65
|
+
@match_code = @compiler.compile_as_node_pattern(@ast, var: VAR)
|
66
|
+
@cache = {}
|
892
67
|
end
|
893
68
|
|
894
|
-
def match(*args, **rest)
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
69
|
+
def match(*args, **rest, &block)
|
70
|
+
@cache[:lambda] ||= as_lambda
|
71
|
+
@cache[:lambda].call(*args, block: block, **rest)
|
72
|
+
end
|
73
|
+
|
74
|
+
def ==(other)
|
75
|
+
other.is_a?(NodePattern) && other.ast == ast
|
76
|
+
end
|
77
|
+
alias eql? ==
|
78
|
+
|
79
|
+
def to_s
|
80
|
+
"#<#{self.class} #{pattern}>"
|
903
81
|
end
|
904
82
|
|
905
|
-
def marshal_load(pattern)
|
83
|
+
def marshal_load(pattern) #:nodoc:
|
906
84
|
initialize pattern
|
907
85
|
end
|
908
86
|
|
909
|
-
def marshal_dump
|
87
|
+
def marshal_dump #:nodoc:
|
910
88
|
pattern
|
911
89
|
end
|
912
90
|
|
913
|
-
def
|
914
|
-
|
915
|
-
Compiler.tokens(other.pattern) == Compiler.tokens(pattern)
|
91
|
+
def as_json(_options = nil) #:nodoc:
|
92
|
+
pattern
|
916
93
|
end
|
917
|
-
alias eql? ==
|
918
94
|
|
919
|
-
def
|
920
|
-
|
95
|
+
def encode_with(coder) #:nodoc:
|
96
|
+
coder['pattern'] = pattern
|
97
|
+
end
|
98
|
+
|
99
|
+
def init_with(coder) #:nodoc:
|
100
|
+
initialize(coder['pattern'])
|
921
101
|
end
|
922
102
|
|
923
103
|
# Yields its argument and any descendants, depth-first.
|
@@ -936,17 +116,11 @@ module RuboCop
|
|
936
116
|
nil
|
937
117
|
end
|
938
118
|
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
end
|
944
|
-
|
945
|
-
def ===(compare)
|
946
|
-
@block.call(compare)
|
947
|
-
end
|
119
|
+
def freeze
|
120
|
+
@match_code.freeze
|
121
|
+
@compiler.freeze
|
122
|
+
super
|
948
123
|
end
|
949
124
|
end
|
950
125
|
end
|
951
126
|
end
|
952
|
-
# rubocop:enable Metrics/ClassLength, Metrics/CyclomaticComplexity
|