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