rubocop-ast 0.5.1 → 1.0.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 +17 -0
- data/lib/rubocop/ast/builder.rb +1 -0
- data/lib/rubocop/ast/node.rb +44 -125
- data/lib/rubocop/ast/node/array_node.rb +1 -0
- data/lib/rubocop/ast/node/block_node.rb +1 -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 +2 -0
- 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/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_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 +5 -1
- data/lib/rubocop/ast/traversal.rb +149 -172
- data/lib/rubocop/ast/version.rb +1 -1
- metadata +37 -3
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module AST
|
5
|
+
class NodePattern
|
6
|
+
# Responsible to build the AST nodes for `NodePattern`
|
7
|
+
#
|
8
|
+
# Doc on how this fits in the compiling process:
|
9
|
+
# /doc/modules/ROOT/pages/node_pattern.md
|
10
|
+
class Builder
|
11
|
+
def emit_capture(capture_token, node)
|
12
|
+
return node if capture_token.nil?
|
13
|
+
|
14
|
+
emit_unary_op(:capture, capture_token, node)
|
15
|
+
end
|
16
|
+
|
17
|
+
def emit_atom(type, value)
|
18
|
+
n(type, [value])
|
19
|
+
end
|
20
|
+
|
21
|
+
def emit_unary_op(type, _operator = nil, *children)
|
22
|
+
n(type, children)
|
23
|
+
end
|
24
|
+
|
25
|
+
def emit_list(type, _begin, children, _end)
|
26
|
+
n(type, children)
|
27
|
+
end
|
28
|
+
|
29
|
+
def emit_call(type, selector, args = nil)
|
30
|
+
_begin_t, arg_nodes, _end_t = args
|
31
|
+
n(type, [selector, *arg_nodes])
|
32
|
+
end
|
33
|
+
|
34
|
+
def emit_union(begin_t, pattern_lists, end_t)
|
35
|
+
children = union_children(pattern_lists)
|
36
|
+
|
37
|
+
type = optimizable_as_set?(children) ? :set : :union
|
38
|
+
emit_list(type, begin_t, children, end_t)
|
39
|
+
end
|
40
|
+
|
41
|
+
def emit_subsequence(node_list)
|
42
|
+
return node_list.first if node_list.size == 1 # Don't put a single child in a subsequence
|
43
|
+
|
44
|
+
emit_list(:subsequence, nil, node_list, nil)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def optimizable_as_set?(children)
|
50
|
+
children.all?(&:matches_within_set?)
|
51
|
+
end
|
52
|
+
|
53
|
+
def n(type, *args)
|
54
|
+
Node::MAP[type].new(type, *args)
|
55
|
+
end
|
56
|
+
|
57
|
+
def union_children(pattern_lists)
|
58
|
+
if pattern_lists.size == 1 # {a b c} => [[a, b, c]] => [a, b, c]
|
59
|
+
children = pattern_lists.first
|
60
|
+
raise NodePattern::Invalid, 'A union can not be empty' if children.empty?
|
61
|
+
|
62
|
+
children
|
63
|
+
else # { a b | c } => [[a, b], [c]] => [s(:subsequence, a, b), c]
|
64
|
+
pattern_lists.map do |list|
|
65
|
+
emit_subsequence(list)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module AST
|
5
|
+
class NodePattern
|
6
|
+
# A NodePattern comment, simplified version of ::Parser::Source::Comment
|
7
|
+
class Comment
|
8
|
+
attr_reader :location
|
9
|
+
alias loc location
|
10
|
+
|
11
|
+
##
|
12
|
+
# @param [Parser::Source::Range] range
|
13
|
+
#
|
14
|
+
def initialize(range)
|
15
|
+
@location = ::Parser::Source::Map.new(range)
|
16
|
+
freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [String]
|
20
|
+
def text
|
21
|
+
loc.expression.source.freeze
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# Compares comments. Two comments are equal if they
|
26
|
+
# correspond to the same source range.
|
27
|
+
#
|
28
|
+
# @param [Object] other
|
29
|
+
# @return [Boolean]
|
30
|
+
#
|
31
|
+
def ==(other)
|
32
|
+
other.is_a?(Comment) &&
|
33
|
+
@location == other.location
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# @return [String] a human-readable representation of this comment
|
38
|
+
#
|
39
|
+
def inspect
|
40
|
+
"#<NodePattern::Comment #{@location.expression} #{text.inspect}>"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module AST
|
5
|
+
class NodePattern
|
6
|
+
# The top-level compiler holding the global state
|
7
|
+
# Defers work to its subcompilers
|
8
|
+
#
|
9
|
+
# Doc on how this fits in the compiling process:
|
10
|
+
# /doc/modules/ROOT/pages/node_pattern.md
|
11
|
+
class Compiler
|
12
|
+
extend Forwardable
|
13
|
+
attr_reader :captures, :named_parameters, :positional_parameters, :binding
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@temp_depth = 0 # avoid name clashes between temp variables
|
17
|
+
@captures = 0 # number of captures seen
|
18
|
+
@positional_parameters = 0 # highest % (param) number seen
|
19
|
+
@named_parameters = Set[] # keyword parameters
|
20
|
+
@binding = Binding.new # bound variables
|
21
|
+
@atom_subcompiler = self.class::AtomSubcompiler.new(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
def_delegators :binding, :bind
|
25
|
+
|
26
|
+
def positional_parameter(number)
|
27
|
+
@positional_parameters = number if number > @positional_parameters
|
28
|
+
"param#{number}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def named_parameter(name)
|
32
|
+
@named_parameters << name
|
33
|
+
name
|
34
|
+
end
|
35
|
+
|
36
|
+
# Enumerates `enum` while keeping track of state accross
|
37
|
+
# union branches (captures and unification).
|
38
|
+
def each_union(enum, &block)
|
39
|
+
enforce_same_captures(binding.union_bind(enum), &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
def compile_as_atom(node)
|
43
|
+
@atom_subcompiler.compile(node)
|
44
|
+
end
|
45
|
+
|
46
|
+
def compile_as_node_pattern(node, **options)
|
47
|
+
self.class::NodePatternSubcompiler.new(self, **options).compile(node)
|
48
|
+
end
|
49
|
+
|
50
|
+
def compile_sequence(sequence, var:)
|
51
|
+
self.class::SequenceSubcompiler.new(self, sequence: sequence, var: var).compile_sequence
|
52
|
+
end
|
53
|
+
|
54
|
+
def parser
|
55
|
+
@parser ||= Parser.new
|
56
|
+
end
|
57
|
+
|
58
|
+
# Utilities
|
59
|
+
|
60
|
+
def with_temp_variables(*names, &block)
|
61
|
+
@temp_depth += 1
|
62
|
+
suffix = @temp_depth if @temp_depth > 1
|
63
|
+
names = block.parameters.map(&:last) if names.empty?
|
64
|
+
names.map! { |name| "#{name}#{suffix}" }
|
65
|
+
yield(*names)
|
66
|
+
ensure
|
67
|
+
@temp_depth -= 1
|
68
|
+
end
|
69
|
+
|
70
|
+
def next_capture
|
71
|
+
"captures[#{new_capture}]"
|
72
|
+
end
|
73
|
+
|
74
|
+
def freeze
|
75
|
+
@named_parameters.freeze
|
76
|
+
super
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def enforce_same_captures(enum)
|
82
|
+
return to_enum __method__, enum unless block_given?
|
83
|
+
|
84
|
+
captures_before = captures_after = nil
|
85
|
+
enum.each do |node|
|
86
|
+
captures_before ||= @captures
|
87
|
+
@captures = captures_before
|
88
|
+
yield node
|
89
|
+
captures_after ||= @captures
|
90
|
+
if captures_after != @captures
|
91
|
+
raise Invalid, 'each branch must have same number of captures'
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def new_capture
|
97
|
+
@captures
|
98
|
+
ensure
|
99
|
+
@captures += 1
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module AST
|
5
|
+
class NodePattern
|
6
|
+
class Compiler
|
7
|
+
# Generates code that evaluates to a value (Ruby object)
|
8
|
+
# This value responds to `===`.
|
9
|
+
#
|
10
|
+
# Doc on how this fits in the compiling process:
|
11
|
+
# /doc/modules/ROOT/pages/node_pattern.md
|
12
|
+
class AtomSubcompiler < Subcompiler
|
13
|
+
private
|
14
|
+
|
15
|
+
def visit_unify
|
16
|
+
compiler.bind(node.child) do
|
17
|
+
raise Invalid, 'unified variables can not appear first as argument'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def visit_symbol
|
22
|
+
node.child.inspect
|
23
|
+
end
|
24
|
+
alias visit_number visit_symbol
|
25
|
+
alias visit_string visit_symbol
|
26
|
+
alias visit_regexp visit_symbol
|
27
|
+
|
28
|
+
def visit_const
|
29
|
+
node.child
|
30
|
+
end
|
31
|
+
|
32
|
+
def visit_named_parameter
|
33
|
+
compiler.named_parameter(node.child)
|
34
|
+
end
|
35
|
+
|
36
|
+
def visit_positional_parameter
|
37
|
+
compiler.positional_parameter(node.child)
|
38
|
+
end
|
39
|
+
|
40
|
+
def visit_set
|
41
|
+
set = node.children.map(&:child).to_set.freeze
|
42
|
+
NodePattern::Sets[set]
|
43
|
+
end
|
44
|
+
|
45
|
+
# Assumes other types are node patterns.
|
46
|
+
def visit_other_type
|
47
|
+
compiler.with_temp_variables do |compare|
|
48
|
+
code = compiler.compile_as_node_pattern(node, var: compare)
|
49
|
+
"->(#{compare}) { #{code} }"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module AST
|
5
|
+
class NodePattern
|
6
|
+
class Compiler
|
7
|
+
# Holds the list of bound variable names
|
8
|
+
class Binding
|
9
|
+
def initialize
|
10
|
+
@bound = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
# Yields the first time a given name is bound
|
14
|
+
#
|
15
|
+
# @return [String] bound variable name
|
16
|
+
def bind(name)
|
17
|
+
var = @bound.fetch(name) do
|
18
|
+
yield n = @bound[name] = "unify_#{name.gsub('-', '__')}"
|
19
|
+
n
|
20
|
+
end
|
21
|
+
|
22
|
+
if var == :forbidden_unification
|
23
|
+
raise Invalid, "Wildcard #{name} was first seen in a subset of a" \
|
24
|
+
" union and can't be used outside that union"
|
25
|
+
end
|
26
|
+
var
|
27
|
+
end
|
28
|
+
|
29
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
30
|
+
def union_bind(enum)
|
31
|
+
# We need to reset @bound before each branch is processed.
|
32
|
+
# Moreover we need to keep track of newly encountered wildcards.
|
33
|
+
# Var `newly_bound_intersection` will hold those that are encountered
|
34
|
+
# in all branches; these are not a problem.
|
35
|
+
# Var `partially_bound` will hold those encountered in only a subset
|
36
|
+
# of the branches; these can't be used outside of the union.
|
37
|
+
|
38
|
+
return to_enum __method__, enum unless block_given?
|
39
|
+
|
40
|
+
newly_bound_intersection = nil
|
41
|
+
partially_bound = []
|
42
|
+
bound_before = @bound.dup
|
43
|
+
|
44
|
+
result = enum.each do |e|
|
45
|
+
@bound = bound_before.dup if newly_bound_intersection
|
46
|
+
yield e
|
47
|
+
newly_bound = @bound.keys - bound_before.keys
|
48
|
+
if newly_bound_intersection.nil?
|
49
|
+
# First iteration
|
50
|
+
newly_bound_intersection = newly_bound
|
51
|
+
else
|
52
|
+
union = newly_bound_intersection | newly_bound
|
53
|
+
newly_bound_intersection &= newly_bound
|
54
|
+
partially_bound |= union - newly_bound_intersection
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# At this point, all members of `newly_bound_intersection` can be used
|
59
|
+
# for unification outside of the union, but partially_bound may not
|
60
|
+
|
61
|
+
forbid(partially_bound)
|
62
|
+
|
63
|
+
result
|
64
|
+
end
|
65
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def forbid(names)
|
70
|
+
names.each do |name|
|
71
|
+
@bound[name] = :forbidden_unification
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rainbow'
|
4
|
+
|
5
|
+
module RuboCop
|
6
|
+
module AST
|
7
|
+
class NodePattern
|
8
|
+
class Compiler
|
9
|
+
# Variant of the Compiler with tracing information for nodes
|
10
|
+
class Debug < Compiler
|
11
|
+
# Compiled node pattern requires a named parameter `trace`,
|
12
|
+
# which should be an instance of this class
|
13
|
+
class Trace
|
14
|
+
def initialize
|
15
|
+
@visit = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def enter(node_id)
|
19
|
+
@visit[node_id] = false
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def success(node_id)
|
24
|
+
@visit[node_id] = true
|
25
|
+
end
|
26
|
+
|
27
|
+
# return nil (not visited), false (not matched) or true (matched)
|
28
|
+
def matched?(node_id)
|
29
|
+
@visit[node_id]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :node_ids
|
34
|
+
|
35
|
+
# @api private
|
36
|
+
class Colorizer
|
37
|
+
COLOR_SCHEME = {
|
38
|
+
not_visitable: :lightseagreen,
|
39
|
+
nil => :yellow,
|
40
|
+
false => :red,
|
41
|
+
true => :green
|
42
|
+
}.freeze
|
43
|
+
|
44
|
+
# Result of a NodePattern run against a particular AST
|
45
|
+
# Consider constructor is private
|
46
|
+
Result = Struct.new(:colorizer, :trace, :returned, :ruby_ast) do
|
47
|
+
# @return [String] a Rainbow colorized version of ruby
|
48
|
+
def colorize(color_scheme = COLOR_SCHEME)
|
49
|
+
map = color_map(color_scheme)
|
50
|
+
ast.loc.expression.source_buffer.source.chars.map.with_index do |char, i|
|
51
|
+
Rainbow(char).color(map[i])
|
52
|
+
end.join
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [Hash] a map for {character_position => color}
|
56
|
+
def color_map(color_scheme = COLOR_SCHEME)
|
57
|
+
@color_map ||=
|
58
|
+
match_map
|
59
|
+
.transform_values { |matched| color_scheme.fetch(matched) }
|
60
|
+
.map { |node, color| color_map_for(node, color) }
|
61
|
+
.inject(:merge)
|
62
|
+
.tap { |h| h.default = color_scheme.fetch(:not_visitable) }
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [Hash] a map for {node => matched?}, depth-first
|
66
|
+
def match_map
|
67
|
+
@match_map ||=
|
68
|
+
ast
|
69
|
+
.each_node
|
70
|
+
.to_h { |node| [node, matched?(node)] }
|
71
|
+
end
|
72
|
+
|
73
|
+
# @return a value of `Trace#matched?` or `:not_visitable`
|
74
|
+
def matched?(node)
|
75
|
+
id = colorizer.compiler.node_ids.fetch(node) { return :not_visitable }
|
76
|
+
trace.matched?(id)
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def color_map_for(node, color)
|
82
|
+
return {} unless (range = node.loc&.expression)
|
83
|
+
|
84
|
+
range.to_a.to_h { |char| [char, color] }
|
85
|
+
end
|
86
|
+
|
87
|
+
def ast
|
88
|
+
colorizer.node_pattern.ast
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
Compiler = Debug
|
93
|
+
|
94
|
+
attr_reader :pattern, :compiler, :node_pattern
|
95
|
+
|
96
|
+
def initialize(pattern, compiler: self.class::Compiler.new)
|
97
|
+
@pattern = pattern
|
98
|
+
@compiler = compiler
|
99
|
+
@node_pattern = ::RuboCop::AST::NodePattern.new(pattern, compiler: @compiler)
|
100
|
+
end
|
101
|
+
|
102
|
+
# @return [Node] the Ruby AST
|
103
|
+
def test(ruby, trace: self.class::Compiler::Trace.new)
|
104
|
+
ruby = ruby_ast(ruby) if ruby.is_a?(String)
|
105
|
+
returned = @node_pattern.as_lambda.call(ruby, trace: trace)
|
106
|
+
self.class::Result.new(self, trace, returned, ruby)
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def ruby_ast(ruby)
|
112
|
+
buffer = ::Parser::Source::Buffer.new('(ruby)', source: ruby)
|
113
|
+
ruby_parser.parse(buffer)
|
114
|
+
end
|
115
|
+
|
116
|
+
def ruby_parser
|
117
|
+
require 'parser/current'
|
118
|
+
builder = ::RuboCop::AST::Builder.new
|
119
|
+
::Parser::CurrentRuby.new(builder)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def initialize
|
124
|
+
super
|
125
|
+
@node_ids = Hash.new { |h, k| h[k] = h.size }.compare_by_identity
|
126
|
+
end
|
127
|
+
|
128
|
+
def named_parameters
|
129
|
+
super << :trace
|
130
|
+
end
|
131
|
+
|
132
|
+
def parser
|
133
|
+
@parser ||= Parser::WithMeta.new
|
134
|
+
end
|
135
|
+
|
136
|
+
def_delegators :parser, :comments, :tokens
|
137
|
+
|
138
|
+
# @api private
|
139
|
+
module InstrumentationSubcompiler
|
140
|
+
def do_compile
|
141
|
+
"#{tracer(:enter)} && #{super} && #{tracer(:success)}"
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
def tracer(kind)
|
147
|
+
"trace.#{kind}(#{node_id})"
|
148
|
+
end
|
149
|
+
|
150
|
+
def node_id
|
151
|
+
compiler.node_ids[node]
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# @api private
|
156
|
+
class NodePatternSubcompiler < Compiler::NodePatternSubcompiler
|
157
|
+
include InstrumentationSubcompiler
|
158
|
+
end
|
159
|
+
|
160
|
+
# @api private
|
161
|
+
class SequenceSubcompiler < Compiler::SequenceSubcompiler
|
162
|
+
include InstrumentationSubcompiler
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|