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.
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