rubocop-ast 0.5.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|