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,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module AST
5
+ class NodePattern
6
+ class Compiler
7
+ # Base class for subcompilers
8
+ # Implements visitor pattern
9
+ #
10
+ # Doc on how this fits in the compiling process:
11
+ # /doc/modules/ROOT/pages/node_pattern.md
12
+ class Subcompiler
13
+ attr_reader :compiler
14
+
15
+ def initialize(compiler)
16
+ @compiler = compiler
17
+ @node = nil
18
+ end
19
+
20
+ def compile(node)
21
+ prev = @node
22
+ @node = node
23
+ do_compile
24
+ ensure
25
+ @node = prev
26
+ end
27
+
28
+ # @api private
29
+
30
+ private
31
+
32
+ attr_reader :node
33
+
34
+ def do_compile
35
+ send(self.class.registry.fetch(node.type, :visit_other_type))
36
+ end
37
+
38
+ @registry = {}
39
+ class << self
40
+ attr_reader :registry
41
+
42
+ def method_added(method)
43
+ @registry[Regexp.last_match(1).to_sym] = method if method =~ /^visit_(.*)/
44
+ super
45
+ end
46
+
47
+ def inherited(base)
48
+ us = self
49
+ base.class_eval { @registry = us.registry.dup }
50
+ super
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require_relative 'lexer.rex'
5
+ rescue LoadError
6
+ msg = '*** You must run `rake generate` to generate the lexer and the parser ***'
7
+ puts '*' * msg.length, msg, '*' * msg.length
8
+ raise
9
+ end
10
+
11
+ module RuboCop
12
+ module AST
13
+ class NodePattern
14
+ # Lexer class for `NodePattern`
15
+ #
16
+ # Doc on how this fits in the compiling process:
17
+ # /doc/modules/ROOT/pages/node_pattern.md
18
+ class Lexer < LexerRex
19
+ Error = ScanError
20
+
21
+ REGEXP_OPTIONS = {
22
+ 'i' => ::Regexp::IGNORECASE,
23
+ 'm' => ::Regexp::MULTILINE,
24
+ 'x' => ::Regexp::EXTENDED,
25
+ 'o' => 0
26
+ }.freeze
27
+ private_constant :REGEXP_OPTIONS
28
+
29
+ attr_reader :source_buffer, :comments, :tokens
30
+
31
+ def initialize(source)
32
+ @tokens = []
33
+ super()
34
+ parse(source)
35
+ end
36
+
37
+ private
38
+
39
+ # @return [token]
40
+ def emit(type)
41
+ value = ss[1] || ss.matched
42
+ value = yield value if block_given?
43
+ token = token(type, value)
44
+ @tokens << token
45
+ token
46
+ end
47
+
48
+ def emit_comment
49
+ nil
50
+ end
51
+
52
+ def emit_regexp
53
+ body = ss[1]
54
+ options = ss[2]
55
+ flag = options.each_char.map { |c| REGEXP_OPTIONS[c] }.sum
56
+
57
+ emit(:tREGEXP) { Regexp.new(body, flag) }
58
+ end
59
+
60
+ def do_parse
61
+ # Called by the generated `parse` method, do nothing here.
62
+ end
63
+
64
+ def token(type, value)
65
+ [type, value]
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,39 @@
1
+ # The only difficulty is to distinguish: `fn(argument)` from `fn (sequence)`.
2
+ # The presence of the whitespace determines if it is an _argument_ to the
3
+ # function call `fn` or if a _sequence_ follows the function call.
4
+ #
5
+ # If there is the potential for an argument list, the lexer enters the state `:ARG`.
6
+ # The rest of the times, the state is `nil`.
7
+ #
8
+ # In case of an argument list, :tARG_LIST is emitted instead of a '('.
9
+ # Therefore, the token '(' always signals the beginning of a sequence.
10
+
11
+ class RuboCop::AST::NodePattern::LexerRex
12
+
13
+ macros
14
+ SYMBOL_NAME /[\w+@*\/?!<>=~|%^-]+|\[\]=?/
15
+ IDENTIFIER /[a-zA-Z_][a-zA-Z0-9_-]*/
16
+ REGEXP_BODY /(?:[^\/]|\\\/)*/
17
+ REGEXP /\/(#{REGEXP_BODY})(?<!\\)\/([imxo]*)/
18
+ rules
19
+ /\s+/
20
+ /:(#{SYMBOL_NAME})/o { emit :tSYMBOL, &:to_sym }
21
+ /"(.+?)"/ { emit :tSTRING }
22
+ /[-+]?\d+\.\d+/ { emit :tNUMBER, &:to_f }
23
+ /[-+]?\d+/ { emit :tNUMBER, &:to_i }
24
+ /#{Regexp.union(
25
+ %w"( ) { | } [ ] < > $ ! ^ ` ... + * ? ,"
26
+ )}/o { emit ss.matched, &:to_sym }
27
+ /#{REGEXP}/o { emit_regexp }
28
+ /%([A-Z:][a-zA-Z_:]+)/ { emit :tPARAM_CONST }
29
+ /%([a-z_]+)/ { emit :tPARAM_NAMED }
30
+ /%(\d*)/ { emit(:tPARAM_NUMBER) { |s| s.empty? ? 1 : s.to_i } } # Map `%` to `%1`
31
+ /_(#{IDENTIFIER})/o { emit :tUNIFY }
32
+ /_/o { emit :tWILDCARD }
33
+ /\#(#{IDENTIFIER}[!?]?)/o { @state = :ARG; emit :tFUNCTION_CALL, &:to_sym }
34
+ /#{IDENTIFIER}\?/o { @state = :ARG; emit :tPREDICATE, &:to_sym }
35
+ /#{IDENTIFIER}/o { emit :tNODE_TYPE, &:to_sym }
36
+ :ARG /\(/ { @state = nil; emit :tARG_LIST }
37
+ :ARG // { @state = nil }
38
+ /\#.*/ { emit_comment }
39
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+ # encoding: UTF-8
3
+ #--
4
+ # This file is automatically generated. Do not modify it.
5
+ # Generated by: oedipus_lex version 2.5.2.
6
+ # Source: lib/rubocop/ast/node_pattern/lexer.rex
7
+ #++
8
+
9
+ # The only difficulty is to distinguish: `fn(argument)` from `fn (sequence)`.
10
+ # The presence of the whitespace determines if it is an _argument_ to the
11
+ # function call `fn` or if a _sequence_ follows the function call.
12
+ #
13
+ # If there is the potential for an argument list, the lexer enters the state `:ARG`.
14
+ # The rest of the times, the state is `nil`.
15
+ #
16
+ # In case of an argument list, :tARG_LIST is emitted instead of a '('.
17
+ # Therefore, the token '(' always signals the beginning of a sequence.
18
+
19
+
20
+ ##
21
+ # The generated lexer RuboCop::AST::NodePattern::LexerRex
22
+
23
+ class RuboCop::AST::NodePattern::LexerRex
24
+ require 'strscan'
25
+
26
+ # :stopdoc:
27
+ SYMBOL_NAME = /[\w+@*\/?!<>=~|%^-]+|\[\]=?/
28
+ IDENTIFIER = /[a-zA-Z_][a-zA-Z0-9_-]*/
29
+ REGEXP_BODY = /(?:[^\/]|\\\/)*/
30
+ REGEXP = /\/(#{REGEXP_BODY})(?<!\\)\/([imxo]*)/
31
+ # :startdoc:
32
+ # :stopdoc:
33
+ class LexerError < StandardError ; end
34
+ class ScanError < LexerError ; end
35
+ # :startdoc:
36
+
37
+ ##
38
+ # The file name / path
39
+
40
+ attr_accessor :filename
41
+
42
+ ##
43
+ # The StringScanner for this lexer.
44
+
45
+ attr_accessor :ss
46
+
47
+ ##
48
+ # The current lexical state.
49
+
50
+ attr_accessor :state
51
+
52
+ alias :match :ss
53
+
54
+ ##
55
+ # The match groups for the current scan.
56
+
57
+ def matches
58
+ m = (1..9).map { |i| ss[i] }
59
+ m.pop until m[-1] or m.empty?
60
+ m
61
+ end
62
+
63
+ ##
64
+ # Yields on the current action.
65
+
66
+ def action
67
+ yield
68
+ end
69
+
70
+
71
+ ##
72
+ # The current scanner class. Must be overridden in subclasses.
73
+
74
+ def scanner_class
75
+ StringScanner
76
+ end unless instance_methods(false).map(&:to_s).include?("scanner_class")
77
+
78
+ ##
79
+ # Parse the given string.
80
+
81
+ def parse str
82
+ self.ss = scanner_class.new str
83
+ self.state ||= nil
84
+
85
+ do_parse
86
+ end
87
+
88
+ ##
89
+ # Read in and parse the file at +path+.
90
+
91
+ def parse_file path
92
+ self.filename = path
93
+ open path do |f|
94
+ parse f.read
95
+ end
96
+ end
97
+
98
+ ##
99
+ # The current location in the parse.
100
+
101
+ def location
102
+ [
103
+ (filename || "<input>"),
104
+ ].compact.join(":")
105
+ end
106
+
107
+ ##
108
+ # Lex the next token.
109
+
110
+ def next_token
111
+
112
+ token = nil
113
+
114
+ until ss.eos? or token do
115
+ token =
116
+ case state
117
+ when nil then
118
+ case
119
+ when ss.skip(/\s+/) then
120
+ # do nothing
121
+ when ss.skip(/:(#{SYMBOL_NAME})/o) then
122
+ action { emit :tSYMBOL, &:to_sym }
123
+ when ss.skip(/"(.+?)"/) then
124
+ action { emit :tSTRING }
125
+ when ss.skip(/[-+]?\d+\.\d+/) then
126
+ action { emit :tNUMBER, &:to_f }
127
+ when ss.skip(/[-+]?\d+/) then
128
+ action { emit :tNUMBER, &:to_i }
129
+ when ss.skip(/#{Regexp.union(
130
+ %w"( ) { | } [ ] < > $ ! ^ ` ... + * ? ,"
131
+ )}/o) then
132
+ action { emit ss.matched, &:to_sym }
133
+ when ss.skip(/#{REGEXP}/o) then
134
+ action { emit_regexp }
135
+ when ss.skip(/%([A-Z:][a-zA-Z_:]+)/) then
136
+ action { emit :tPARAM_CONST }
137
+ when ss.skip(/%([a-z_]+)/) then
138
+ action { emit :tPARAM_NAMED }
139
+ when ss.skip(/%(\d*)/) then
140
+ action { emit(:tPARAM_NUMBER) { |s| s.empty? ? 1 : s.to_i } } # Map `%` to `%1`
141
+ when ss.skip(/_(#{IDENTIFIER})/o) then
142
+ action { emit :tUNIFY }
143
+ when ss.skip(/_/o) then
144
+ action { emit :tWILDCARD }
145
+ when ss.skip(/\#(#{IDENTIFIER}[!?]?)/o) then
146
+ action { @state = :ARG; emit :tFUNCTION_CALL, &:to_sym }
147
+ when ss.skip(/#{IDENTIFIER}\?/o) then
148
+ action { @state = :ARG; emit :tPREDICATE, &:to_sym }
149
+ when ss.skip(/#{IDENTIFIER}/o) then
150
+ action { emit :tNODE_TYPE, &:to_sym }
151
+ when ss.skip(/\#.*/) then
152
+ action { emit_comment }
153
+ else
154
+ text = ss.string[ss.pos .. -1]
155
+ raise ScanError, "can not match (#{state.inspect}) at #{location}: '#{text}'"
156
+ end
157
+ when :ARG then
158
+ case
159
+ when ss.skip(/\(/) then
160
+ action { @state = nil; emit :tARG_LIST }
161
+ when ss.skip(//) then
162
+ action { @state = nil }
163
+ else
164
+ text = ss.string[ss.pos .. -1]
165
+ raise ScanError, "can not match (#{state.inspect}) at #{location}: '#{text}'"
166
+ end
167
+ else
168
+ raise ScanError, "undefined state at #{location}: '#{state}'"
169
+ end # token = case state
170
+
171
+ next unless token # allow functions to trigger redo w/ nil
172
+ end # while
173
+
174
+ raise LexerError, "bad lexical result at #{location}: #{token.inspect}" unless
175
+ token.nil? || (Array === token && token.size >= 2)
176
+
177
+ # auto-switch state
178
+ self.state = token.last if token && token.first == :state
179
+
180
+ token
181
+ end # def next_token
182
+ end # class
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module AST
5
+ class NodePattern
6
+ # Functionality to turn `match_code` into methods/lambda
7
+ module MethodDefiner
8
+ def def_node_matcher(base, method_name, **defaults)
9
+ def_helper(base, method_name, **defaults) do |name|
10
+ params = emit_params('param0 = self')
11
+ <<~RUBY
12
+ def #{name}(#{params})
13
+ #{VAR} = param0
14
+ #{compile_init}
15
+ #{emit_method_code}
16
+ end
17
+ RUBY
18
+ end
19
+ end
20
+
21
+ def def_node_search(base, method_name, **defaults)
22
+ def_helper(base, method_name, **defaults) do |name|
23
+ emit_node_search(name)
24
+ end
25
+ end
26
+
27
+ def compile_as_lambda
28
+ <<~RUBY
29
+ ->(#{emit_params('param0')}, block: nil) do
30
+ #{VAR} = param0
31
+ #{compile_init}
32
+ #{emit_lambda_code}
33
+ end
34
+ RUBY
35
+ end
36
+
37
+ def as_lambda
38
+ eval(compile_as_lambda) # rubocop:disable Security/Eval
39
+ end
40
+
41
+ private
42
+
43
+ # This method minimizes the closure for our method
44
+ def wrapping_block(method_name, **defaults)
45
+ proc do |*args, **values|
46
+ send method_name, *args, **defaults, **values
47
+ end
48
+ end
49
+
50
+ def def_helper(base, method_name, **defaults)
51
+ location = caller_locations(3, 1).first
52
+ unless defaults.empty?
53
+ call = :"without_defaults_#{method_name}"
54
+ base.send :define_method, method_name, &wrapping_block(call, **defaults)
55
+ method_name = call
56
+ end
57
+ src = yield method_name
58
+ base.class_eval(src, location.path, location.lineno)
59
+ end
60
+
61
+ def emit_node_search(method_name)
62
+ if method_name.to_s.end_with?('?')
63
+ on_match = 'return true'
64
+ else
65
+ args = emit_params(":#{method_name}", 'param0', forwarding: true)
66
+ prelude = "return enum_for(#{args}) unless block_given?\n"
67
+ on_match = emit_yield_capture(VAR)
68
+ end
69
+ emit_node_search_body(method_name, prelude: prelude, on_match: on_match)
70
+ end
71
+
72
+ def emit_node_search_body(method_name, prelude:, on_match:)
73
+ <<~RUBY
74
+ def #{method_name}(#{emit_params('param0')})
75
+ #{compile_init}
76
+ #{prelude}
77
+ param0.each_node do |#{VAR}|
78
+ if #{match_code}
79
+ #{on_match}
80
+ end
81
+ end
82
+ nil
83
+ end
84
+ RUBY
85
+ end
86
+
87
+ def emit_yield_capture(when_no_capture = '', yield_with: 'yield')
88
+ yield_val = if captures.zero?
89
+ when_no_capture
90
+ elsif captures == 1
91
+ 'captures[0]' # Circumvent https://github.com/jruby/jruby/issues/5710
92
+ else
93
+ '*captures'
94
+ end
95
+ "#{yield_with}(#{yield_val})"
96
+ end
97
+
98
+ def emit_retval
99
+ if captures.zero?
100
+ 'true'
101
+ elsif captures == 1
102
+ 'captures[0]'
103
+ else
104
+ 'captures'
105
+ end
106
+ end
107
+
108
+ def emit_param_list
109
+ (1..positional_parameters).map { |n| "param#{n}" }.join(',')
110
+ end
111
+
112
+ def emit_keyword_list(forwarding: false)
113
+ pattern = "%<keyword>s: #{'%<keyword>s' if forwarding}"
114
+ named_parameters.map { |k| format(pattern, keyword: k) }.join(',')
115
+ end
116
+
117
+ def emit_params(*first, forwarding: false)
118
+ params = emit_param_list
119
+ keywords = emit_keyword_list(forwarding: forwarding)
120
+ [*first, params, keywords].reject(&:empty?).join(',')
121
+ end
122
+
123
+ def emit_method_code
124
+ <<~RUBY
125
+ return unless #{match_code}
126
+ block_given? ? #{emit_yield_capture} : (return #{emit_retval})
127
+ RUBY
128
+ end
129
+
130
+ def emit_lambda_code
131
+ <<~RUBY
132
+ return unless #{match_code}
133
+ block ? #{emit_yield_capture(yield_with: 'block.call')} : (return #{emit_retval})
134
+ RUBY
135
+ end
136
+
137
+ def compile_init
138
+ "captures = Array.new(#{captures})" if captures.positive?
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end