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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rubocop/ast.rb +17 -0
  3. data/lib/rubocop/ast/builder.rb +1 -0
  4. data/lib/rubocop/ast/node.rb +44 -125
  5. data/lib/rubocop/ast/node/array_node.rb +1 -0
  6. data/lib/rubocop/ast/node/block_node.rb +1 -0
  7. data/lib/rubocop/ast/node/def_node.rb +5 -0
  8. data/lib/rubocop/ast/node/keyword_splat_node.rb +1 -0
  9. data/lib/rubocop/ast/node/mixin/collection_node.rb +1 -0
  10. data/lib/rubocop/ast/node/mixin/descendence.rb +116 -0
  11. data/lib/rubocop/ast/node/mixin/method_dispatch_node.rb +2 -0
  12. data/lib/rubocop/ast/node/mixin/method_identifier_predicates.rb +9 -0
  13. data/lib/rubocop/ast/node/mixin/numeric_node.rb +1 -0
  14. data/lib/rubocop/ast/node/mixin/predicate_operator_node.rb +7 -3
  15. data/lib/rubocop/ast/node/pair_node.rb +4 -0
  16. data/lib/rubocop/ast/node/regexp_node.rb +9 -4
  17. data/lib/rubocop/ast/node_pattern.rb +44 -870
  18. data/lib/rubocop/ast/node_pattern/builder.rb +72 -0
  19. data/lib/rubocop/ast/node_pattern/comment.rb +45 -0
  20. data/lib/rubocop/ast/node_pattern/compiler.rb +104 -0
  21. data/lib/rubocop/ast/node_pattern/compiler/atom_subcompiler.rb +56 -0
  22. data/lib/rubocop/ast/node_pattern/compiler/binding.rb +78 -0
  23. data/lib/rubocop/ast/node_pattern/compiler/debug.rb +168 -0
  24. data/lib/rubocop/ast/node_pattern/compiler/node_pattern_subcompiler.rb +146 -0
  25. data/lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb +420 -0
  26. data/lib/rubocop/ast/node_pattern/compiler/subcompiler.rb +57 -0
  27. data/lib/rubocop/ast/node_pattern/lexer.rb +70 -0
  28. data/lib/rubocop/ast/node_pattern/lexer.rex +39 -0
  29. data/lib/rubocop/ast/node_pattern/lexer.rex.rb +182 -0
  30. data/lib/rubocop/ast/node_pattern/method_definer.rb +143 -0
  31. data/lib/rubocop/ast/node_pattern/node.rb +275 -0
  32. data/lib/rubocop/ast/node_pattern/parser.racc.rb +470 -0
  33. data/lib/rubocop/ast/node_pattern/parser.rb +66 -0
  34. data/lib/rubocop/ast/node_pattern/parser.y +103 -0
  35. data/lib/rubocop/ast/node_pattern/sets.rb +37 -0
  36. data/lib/rubocop/ast/node_pattern/with_meta.rb +111 -0
  37. data/lib/rubocop/ast/processed_source.rb +5 -1
  38. data/lib/rubocop/ast/traversal.rb +149 -172
  39. data/lib/rubocop/ast/version.rb +1 -1
  40. 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