rubocop-ast 0.0.3 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0c196be6c58699fcb84209860508c96e69e8122adb8e14d6641134009802712
4
- data.tar.gz: a2d50c781f612349b4aa7e24a4ea4008c359ce766a6f5e0f7e925ac78d57dbb7
3
+ metadata.gz: 4108494066548a429e972d3f2afcaa9adfd39072a929308cdb20e4a602fef66d
4
+ data.tar.gz: 413b4382539bffed1760d0599bc093ad40ffef64992f821484994150903812da
5
5
  SHA512:
6
- metadata.gz: 2d263c6a0ab978cb1b81dde253ce752c2b64af9e94c59ac6a6a364aef3cec37c03028e4d7c00086c11b8b46abae9373edad636528782f84ad914ea527b644ec0
7
- data.tar.gz: edeefb4a74f678920907e130ea6a7fea86b4c67cf9a9bbaefa21ef231738c0de541d61de5d9c234384e46b3243a74ffaac84288b2c68c49e3178cb39d2d96b37
6
+ metadata.gz: 97af9dd864e8f6867614a79b2aac596e18e147b9a55bcc67b8755e47ef762730ec69612dcc6858ee12017b33beae0b89e008e1267034f1730682015c4efa4255
7
+ data.tar.gz: 807305ab6dc90885fff96d1387d787debf8ce8f27a0d150ad4bf86a76d4afb2d288f0771b2399cf3c3cfb1472c72cd750e4df9e97ad07467da4615a97c062f79
data/README.md CHANGED
@@ -2,12 +2,15 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/rubocop-ast.svg)](https://badge.fury.io/rb/rubocop-ast)
4
4
  [![CI](https://github.com/rubocop-hq/rubocop-ast/workflows/CI/badge.svg)](https://github.com/rubocop-hq/rubocop-ast/actions?query=workflow%3ACI)
5
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/a29666e6373bc41bc0a9/test_coverage)](https://codeclimate.com/github/rubocop-hq/rubocop-ast/test_coverage)
6
+ [![Maintainability](https://api.codeclimate.com/v1/badges/a29666e6373bc41bc0a9/maintainability)](https://codeclimate.com/github/rubocop-hq/rubocop-ast/maintainability)
5
7
 
6
8
  Contains the classes needed by [RuboCop](https://github.com/rubocop-hq/rubocop) to deal with Ruby's AST, in particular:
7
9
  * `RuboCop::AST::Node`
8
- * `RuboCop::AST::NodePattern` ([doc](manual/node_pattern.md))
10
+ * `RuboCop::AST::NodePattern` ([doc](docs/modules/ROOT/pages/node_pattern.adoc))
9
11
 
10
- This gem may be used independently from the main RuboCop gem.
12
+ This gem may be used independently from the main RuboCop gem. It was extracted from RuboCop in version 0.84 and its only
13
+ dependency is the `parser` gem, which `rubocop-ast` extends.
11
14
 
12
15
  ## Installation
13
16
 
@@ -25,7 +28,17 @@ gem 'rubocop-ast'
25
28
 
26
29
  ## Usage
27
30
 
28
- Refer to the documentation of `RuboCop::AST::Node` and [`RuboCop::AST::NodePattern`](manual/node_pattern.md)
31
+ Refer to the documentation of `RuboCop::AST::Node` and [`RuboCop::AST::NodePattern`](docs/modules/ROOT/pages/node_pattern.adoc)
32
+
33
+ ### Parser compatibility switches
34
+
35
+ The main `RuboCop` gem uses [legacy AST output from parser](https://github.com/whitequark/parser/#usage).
36
+ This gem is meant to be compatible with all settings. For example, to have `-> { ... }` emitted
37
+ as `LambdaNode` instead of `SendNode`:
38
+
39
+ ```ruby
40
+ RuboCop::AST::Builder.emit_lambda = true
41
+ ```
29
42
 
30
43
  ## Contributing
31
44
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'parser'
4
4
  require 'forwardable'
5
+ require 'set'
5
6
 
6
7
  require_relative 'ast/node_pattern'
7
8
  require_relative 'ast/sexp'
@@ -34,8 +35,11 @@ require_relative 'ast/node/forward_args_node'
34
35
  require_relative 'ast/node/float_node'
35
36
  require_relative 'ast/node/hash_node'
36
37
  require_relative 'ast/node/if_node'
38
+ require_relative 'ast/node/index_node'
39
+ require_relative 'ast/node/indexasgn_node'
37
40
  require_relative 'ast/node/int_node'
38
41
  require_relative 'ast/node/keyword_splat_node'
42
+ require_relative 'ast/node/lambda_node'
39
43
  require_relative 'ast/node/module_node'
40
44
  require_relative 'ast/node/or_node'
41
45
  require_relative 'ast/node/pair_node'
@@ -35,9 +35,12 @@ module RuboCop
35
35
  hash: HashNode,
36
36
  if: IfNode,
37
37
  int: IntNode,
38
+ index: IndexNode,
39
+ indexasgn: IndexasgnNode,
38
40
  irange: RangeNode,
39
41
  erange: RangeNode,
40
42
  kwsplat: KeywordSplatNode,
43
+ lambda: LambdaNode,
41
44
  module: ModuleNode,
42
45
  or: OrNode,
43
46
  pair: PairNode,
@@ -44,6 +44,8 @@ module RuboCop
44
44
 
45
45
  BASIC_CONDITIONALS = %i[if while until].freeze
46
46
  CONDITIONALS = [*BASIC_CONDITIONALS, :case].freeze
47
+ POST_CONDITION_LOOP_TYPES = %i[while_post until_post].freeze
48
+ LOOP_TYPES = (POST_CONDITION_LOOP_TYPES + %i[while until for]).freeze
47
49
  VARIABLES = %i[ivar gvar cvar lvar].freeze
48
50
  REFERENCES = %i[nth_ref back_ref].freeze
49
51
  KEYWORDS = %i[alias and break case class def defs defined?
@@ -53,6 +55,7 @@ module RuboCop
53
55
  yield].freeze
54
56
  OPERATOR_KEYWORDS = %i[and or].freeze
55
57
  SPECIAL_KEYWORDS = %w[__FILE__ __LINE__ __ENCODING__].freeze
58
+ ARGUMENT_TYPES = %i[arg optarg restarg kwarg kwoptarg kwrestarg blockarg].freeze
56
59
 
57
60
  # @see https://www.rubydoc.info/gems/ast/AST/Node:initialize
58
61
  def initialize(type, children = [], properties = {})
@@ -425,6 +428,15 @@ module RuboCop
425
428
  CONDITIONALS.include?(type)
426
429
  end
427
430
 
431
+ def post_condition_loop?
432
+ POST_CONDITION_LOOP_TYPES.include?(type)
433
+ end
434
+
435
+ # Note: `loop { }` is a normal method call and thus not a loop keyword.
436
+ def loop_keyword?
437
+ LOOP_TYPES.include?(type)
438
+ end
439
+
428
440
  def keyword?
429
441
  return true if special_keyword? || send_type? && prefix_not?
430
442
  return false unless KEYWORDS.include?(type)
@@ -456,6 +468,10 @@ module RuboCop
456
468
  parent&.send_type? && parent.arguments.include?(self)
457
469
  end
458
470
 
471
+ def argument_type?
472
+ ARGUMENT_TYPES.include?(type)
473
+ end
474
+
459
475
  def boolean_type?
460
476
  true_type? || false_type?
461
477
  end
@@ -500,7 +516,7 @@ module RuboCop
500
516
  # So, does the return value of this node matter? If we changed it to
501
517
  # `(...; nil)`, might that affect anything?
502
518
  #
503
- # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
519
+ # rubocop:disable Metrics/MethodLength
504
520
  def value_used?
505
521
  # Be conservative and return true if we're not sure.
506
522
  return false if parent.nil?
@@ -522,7 +538,7 @@ module RuboCop
522
538
  true
523
539
  end
524
540
  end
525
- # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity
541
+ # rubocop:enable Metrics/MethodLength
526
542
 
527
543
  # Some expressions are evaluated for their value, some for their side
528
544
  # effects, and some for both.
@@ -24,7 +24,7 @@ module RuboCop
24
24
  #
25
25
  # @return [Boolean] whether the `def` node uses argument forwarding
26
26
  def argument_forwarding?
27
- arguments.any?(&:forward_args_type?)
27
+ arguments.any?(&:forward_args_type?) || arguments.any?(&:forward_arg_type?)
28
28
  end
29
29
 
30
30
  # The name of the defined method as a symbol.
@@ -5,6 +5,21 @@ module RuboCop
5
5
  # A node extension for `forward-args` nodes. This will be used in place
6
6
  # of a plain node when the builder constructs the AST, making its methods
7
7
  # available to all `forward-args` nodes within RuboCop.
8
+ #
9
+ # Not used with modern emitters:
10
+ #
11
+ # $ ruby-parse -e "def foo(...); end"
12
+ # (def :foo
13
+ # (args
14
+ # (forward-arg)) nil)
15
+ # $ ruby-parse --legacy -e "->(foo) { bar }"
16
+ # (def :foo
17
+ # (forward-args) nil)
18
+ #
19
+ # Note the extra 's' with legacy form.
20
+ #
21
+ # The main RuboCop runs in legacy mode; this node is only used
22
+ # if user `AST::Builder.modernize` or `AST::Builder.emit_lambda=true`
8
23
  class ForwardArgsNode < Node
9
24
  include CollectionNode
10
25
 
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module AST
5
+ # Used for modern support only!
6
+ # Not as thoroughly tested as legacy equivalent
7
+ #
8
+ # $ ruby-parse -e "foo[:bar]"
9
+ # (index
10
+ # (send nil :foo)
11
+ # (sym :bar))
12
+ # $ ruby-parse --legacy -e "foo[:bar]"
13
+ # (send
14
+ # (send nil :foo) :[]
15
+ # (sym :bar))
16
+ #
17
+ # The main RuboCop runs in legacy mode; this node is only used
18
+ # if user `AST::Builder.modernize` or `AST::Builder.emit_index=true`
19
+ class IndexNode < Node
20
+ include ParameterizedNode
21
+ include MethodDispatchNode
22
+
23
+ # For similarity with legacy mode
24
+ def attribute_accessor?
25
+ false
26
+ end
27
+
28
+ # For similarity with legacy mode
29
+ def assignment_method?
30
+ false
31
+ end
32
+
33
+ # For similarity with legacy mode
34
+ def method_name
35
+ :[]
36
+ end
37
+
38
+ # An array containing the arguments of the dispatched method.
39
+ #
40
+ # @return [Array<Node>] the arguments of the dispatched method
41
+ def arguments
42
+ node_parts[1..-1]
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module AST
5
+ # Used for modern support only!
6
+ # Not as thoroughly tested as legacy equivalent
7
+ #
8
+ # $ ruby-parse -e "foo[:bar] = :baz"
9
+ # (indexasgn
10
+ # (send nil :foo)
11
+ # (sym :bar)
12
+ # (sym :baz))
13
+ # $ ruby-parse --legacy -e "foo[:bar] = :baz"
14
+ # (send
15
+ # (send nil :foo) :[]=
16
+ # (sym :bar)
17
+ # (sym :baz))
18
+ #
19
+ # The main RuboCop runs in legacy mode; this node is only used
20
+ # if user `AST::Builder.modernize` or `AST::Builder.emit_index=true`
21
+ class IndexasgnNode < Node
22
+ include ParameterizedNode
23
+ include MethodDispatchNode
24
+
25
+ # For similarity with legacy mode
26
+ def attribute_accessor?
27
+ false
28
+ end
29
+
30
+ # For similarity with legacy mode
31
+ def assignment_method?
32
+ true
33
+ end
34
+
35
+ # For similarity with legacy mode
36
+ def method_name
37
+ :[]=
38
+ end
39
+
40
+ # An array containing the arguments of the dispatched method.
41
+ #
42
+ # @return [Array<Node>] the arguments of the dispatched method
43
+ def arguments
44
+ node_parts[1..-1]
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module AST
5
+ # Used for modern support only:
6
+ # Not as thoroughly tested as legacy equivalent
7
+ #
8
+ # $ ruby-parse -e "->(foo) { bar }"
9
+ # (block
10
+ # (lambda)
11
+ # (args
12
+ # (arg :foo))
13
+ # (send nil :bar))
14
+ # $ ruby-parse --legacy -e "->(foo) { bar }"
15
+ # (block
16
+ # (send nil :lambda)
17
+ # (args
18
+ # (arg :foo))
19
+ # (send nil :bar))
20
+ #
21
+ # The main RuboCop runs in legacy mode; this node is only used
22
+ # if user `AST::Builder.modernize` or `AST::Builder.emit_lambda=true`
23
+ class LambdaNode < Node
24
+ include ParameterizedNode
25
+ include MethodDispatchNode
26
+
27
+ # For similarity with legacy mode
28
+ def lambda?
29
+ true
30
+ end
31
+
32
+ # For similarity with legacy mode
33
+ def lambda_literal?
34
+ true
35
+ end
36
+
37
+ # For similarity with legacy mode
38
+ def attribute_accessor?
39
+ false
40
+ end
41
+
42
+ # For similarity with legacy mode
43
+ def assignment_method?
44
+ false
45
+ end
46
+
47
+ # For similarity with legacy mode
48
+ def method_name
49
+ :lambda
50
+ end
51
+
52
+ # For similarity with legacy mode
53
+ def arguments
54
+ []
55
+ end
56
+ end
57
+ end
58
+ end
@@ -3,7 +3,8 @@
3
3
  module RuboCop
4
4
  module AST
5
5
  # Common functionality for nodes that are a kind of method dispatch:
6
- # `send`, `csend`, `super`, `zsuper`, `yield`, `defined?`
6
+ # `send`, `csend`, `super`, `zsuper`, `yield`, `defined?`,
7
+ # and (modern only): `index`, `indexasgn`, `lambda`
7
8
  module MethodDispatchNode
8
9
  extend NodePattern::Macros
9
10
  include MethodIdentifierPredicates
@@ -6,15 +6,62 @@ module RuboCop
6
6
  # `send`, `csend`, `def`, `defs`, `super`, `zsuper`
7
7
  #
8
8
  # @note this mixin expects `#method_name` and `#receiver` to be implemented
9
- module MethodIdentifierPredicates
9
+ module MethodIdentifierPredicates # rubocop:disable Metrics/ModuleLength
10
10
  ENUMERATOR_METHODS = %i[collect collect_concat detect downto each
11
11
  find find_all find_index inject loop map!
12
12
  map reduce reject reject! reverse_each select
13
- select! times upto].freeze
13
+ select! times upto].to_set.freeze
14
+
15
+ ENUMERABLE_METHODS = (Enumerable.instance_methods + [:each]).to_set.freeze
14
16
 
15
17
  # http://phrogz.net/programmingruby/language.html#table_18.4
16
18
  OPERATOR_METHODS = %i[| ^ & <=> == === =~ > >= < <= << >> + - * /
17
- % ** ~ +@ -@ !@ ~@ [] []= ! != !~ `].freeze
19
+ % ** ~ +@ -@ !@ ~@ [] []= ! != !~ `].to_set.freeze
20
+
21
+ NONMUTATING_BINARY_OPERATOR_METHODS = %i[* / % + - == === != < > <= >= <=>].to_set.freeze
22
+ NONMUTATING_UNARY_OPERATOR_METHODS = %i[+@ -@ ~ !].to_set.freeze
23
+ NONMUTATING_OPERATOR_METHODS = (NONMUTATING_BINARY_OPERATOR_METHODS +
24
+ NONMUTATING_UNARY_OPERATOR_METHODS).freeze
25
+
26
+ NONMUTATING_ARRAY_METHODS = %i[
27
+ all? any? assoc at bsearch bsearch_index collect
28
+ combination compact count cycle deconstruct difference
29
+ dig drop drop_while each each_index empty? eql?
30
+ fetch filter find_index first flatten hash
31
+ include? index inspect intersection join
32
+ last length map max min minmax none? one? pack
33
+ permutation product rassoc reject
34
+ repeated_combination repeated_permutation reverse
35
+ reverse_each rindex rotate sample select shuffle
36
+ size slice sort sum take take_while
37
+ to_a to_ary to_h to_s transpose union uniq
38
+ values_at zip |
39
+ ].to_set.freeze
40
+
41
+ NONMUTATING_HASH_METHODS = %i[
42
+ any? assoc compact dig each each_key each_pair
43
+ each_value empty? eql? fetch fetch_values filter
44
+ flatten has_key? has_value? hash include? inspect
45
+ invert key key? keys? length member? merge rassoc
46
+ rehash reject select size slice to_a to_h to_hash
47
+ to_proc to_s transform_keys transform_values value?
48
+ values values_at
49
+ ].to_set.freeze
50
+
51
+ NONMUTATING_STRING_METHODS = %i[
52
+ ascii_only? b bytes bytesize byteslice capitalize
53
+ casecmp casecmp? center chars chomp chop chr codepoints
54
+ count crypt delete delete_prefix delete_suffix
55
+ downcase dump each_byte each_char each_codepoint
56
+ each_grapheme_cluster each_line empty? encode encoding
57
+ end_with? eql? getbyte grapheme_clusters gsub hash
58
+ hex include index inspect intern length lines ljust lstrip
59
+ match match? next oct ord partition reverse rindex rjust
60
+ rpartition rstrip scan scrub size slice squeeze start_with?
61
+ strip sub succ sum swapcase to_a to_c to_f to_i to_r to_s
62
+ to_str to_sym tr tr_s unicode_normalize unicode_normalized?
63
+ unpack unpack1 upcase upto valid_encoding?
64
+ ].to_set.freeze
18
65
 
19
66
  # Checks whether the method name matches the argument.
20
67
  #
@@ -31,6 +78,48 @@ module RuboCop
31
78
  OPERATOR_METHODS.include?(method_name)
32
79
  end
33
80
 
81
+ # Checks whether the method is a nonmutating binary operator method.
82
+ #
83
+ # @return [Boolean] whether the method is a nonmutating binary operator method
84
+ def nonmutating_binary_operator_method?
85
+ NONMUTATING_BINARY_OPERATOR_METHODS.include?(method_name)
86
+ end
87
+
88
+ # Checks whether the method is a nonmutating unary operator method.
89
+ #
90
+ # @return [Boolean] whether the method is a nonmutating unary operator method
91
+ def nonmutating_unary_operator_method?
92
+ NONMUTATING_UNARY_OPERATOR_METHODS.include?(method_name)
93
+ end
94
+
95
+ # Checks whether the method is a nonmutating operator method.
96
+ #
97
+ # @return [Boolean] whether the method is a nonmutating operator method
98
+ def nonmutating_operator_method?
99
+ NONMUTATING_OPERATOR_METHODS.include?(method_name)
100
+ end
101
+
102
+ # Checks whether the method is a nonmutating Array method.
103
+ #
104
+ # @return [Boolean] whether the method is a nonmutating Array method
105
+ def nonmutating_array_method?
106
+ NONMUTATING_ARRAY_METHODS.include?(method_name)
107
+ end
108
+
109
+ # Checks whether the method is a nonmutating Hash method.
110
+ #
111
+ # @return [Boolean] whether the method is a nonmutating Hash method
112
+ def nonmutating_hash_method?
113
+ NONMUTATING_HASH_METHODS.include?(method_name)
114
+ end
115
+
116
+ # Checks whether the method is a nonmutating String method.
117
+ #
118
+ # @return [Boolean] whether the method is a nonmutating String method
119
+ def nonmutating_string_method?
120
+ NONMUTATING_STRING_METHODS.include?(method_name)
121
+ end
122
+
34
123
  # Checks whether the method is a comparison method.
35
124
  #
36
125
  # @return [Boolean] whether the method is a comparison
@@ -53,6 +142,13 @@ module RuboCop
53
142
  method_name.to_s.start_with?('each_')
54
143
  end
55
144
 
145
+ # Checks whether the method is an Enumerable method.
146
+ #
147
+ # @return [Boolean] whether the method is an Enumerable method
148
+ def enumerable_method?
149
+ ENUMERABLE_METHODS.include?(method_name)
150
+ end
151
+
56
152
  # Checks whether the method is a predicate method.
57
153
  #
58
154
  # @return [Boolean] whether the method is a predicate method
@@ -4,6 +4,7 @@ module RuboCop
4
4
  module AST
5
5
  # Common functionality for nodes that are parameterized:
6
6
  # `send`, `super`, `zsuper`, `def`, `defs`
7
+ # and (modern only): `index`, `indexasgn`, `lambda`
7
8
  module ParameterizedNode
8
9
  # Checks whether this node's arguments are wrapped in parentheses.
9
10
  #
@@ -31,6 +31,62 @@ module RuboCop
31
31
  def content
32
32
  children.select(&:str_type?).map(&:str_content).join
33
33
  end
34
+
35
+ # @return [Bool] if the regexp is a /.../ literal
36
+ def slash_literal?
37
+ loc.begin.source == '/'
38
+ end
39
+
40
+ # @return [Bool] if the regexp is a %r{...} literal (using any delimiters)
41
+ def percent_r_literal?
42
+ !slash_literal?
43
+ end
44
+
45
+ # @return [String] the regexp delimiters (without %r)
46
+ def delimiters
47
+ [loc.begin.source[-1], loc.end.source[0]]
48
+ end
49
+
50
+ # @return [Bool] if char is one of the delimiters
51
+ def delimiter?(char)
52
+ delimiters.include?(char)
53
+ end
54
+
55
+ # @return [Bool] if regexp contains interpolation
56
+ def interpolation?
57
+ children.any?(&:begin_type?)
58
+ end
59
+
60
+ # @return [Bool] if regexp uses the multiline regopt
61
+ def multiline_mode?
62
+ regopt_include?(:m)
63
+ end
64
+
65
+ # @return [Bool] if regexp uses the extended regopt
66
+ def extended?
67
+ regopt_include?(:x)
68
+ end
69
+
70
+ # @return [Bool] if regexp uses the ignore-case regopt
71
+ def ignore_case?
72
+ regopt_include?(:i)
73
+ end
74
+
75
+ # @return [Bool] if regexp uses the single-interpolation regopt
76
+ def single_interpolation?
77
+ regopt_include?(:o)
78
+ end
79
+
80
+ # @return [Bool] if regexp uses the no-encoding regopt
81
+ def no_encoding?
82
+ regopt_include?(:n)
83
+ end
84
+
85
+ private
86
+
87
+ def regopt_include?(option)
88
+ regopt.children.include?(option)
89
+ end
34
90
  end
35
91
  end
36
92
  end
@@ -10,7 +10,8 @@ module RuboCop
10
10
  include MethodDispatchNode
11
11
 
12
12
  def_node_matcher :attribute_accessor?, <<~PATTERN
13
- (send nil? ${:attr_reader :attr_writer :attr_accessor :attr} $...)
13
+ [(send nil? ${:attr_reader :attr_writer :attr_accessor :attr} $...)
14
+ (_ _ _ _ ...)]
14
15
  PATTERN
15
16
  end
16
17
  end
@@ -70,13 +70,23 @@ module RuboCop
70
70
  # '(send %1 _)' # % stands for a parameter which must be supplied to
71
71
  # # #match at matching time
72
72
  # # it will be compared to the corresponding value in
73
- # # the AST using #==
73
+ # # the AST using #=== so you can pass Procs, Regexp,
74
+ # # etc. in addition to Nodes or literals.
75
+ # # `Array#===` will never match a node element, but
76
+ # # `Set#===` is an alias to `Set#include?` (Ruby 2.5+
77
+ # # only), and so can be very useful to match within
78
+ # # many possible literals / Nodes.
74
79
  # # a bare '%' is the same as '%1'
75
80
  # # the number of extra parameters passed to #match
76
81
  # # must equal the highest % value in the pattern
77
82
  # # for consistency, %0 is the 'root node' which is
78
83
  # # passed as the 1st argument to #match, where the
79
84
  # # matching process starts
85
+ # '(send _ %named)' # arguments can also be passed as named
86
+ # # parameters (see `%1`)
87
+ # # Note that the macros `def_node_pattern` and
88
+ # # `def_node_search` accept default values for these.
89
+ # '(send _ %CONST)' # the named constant will act like `%1` and `%named`.
80
90
  # '^^send' # each ^ ascends one level in the AST
81
91
  # # so this matches against the grandparent node
82
92
  # '`send' # descends any number of level in the AST
@@ -119,11 +129,13 @@ module RuboCop
119
129
  ).freeze
120
130
  NUMBER = /-?\d+(?:\.\d+)?/.freeze
121
131
  STRING = /".+?"/.freeze
122
- METHOD_NAME = /\#?#{IDENTIFIER}[\!\?]?\(?/.freeze
132
+ METHOD_NAME = /\#?#{IDENTIFIER}[!?]?\(?/.freeze
133
+ PARAM_CONST = /%[A-Z:][a-zA-Z_:]+/.freeze
134
+ KEYWORD_NAME = /%[a-z_]+/.freeze
123
135
  PARAM_NUMBER = /%\d*/.freeze
124
136
 
125
- SEPARATORS = /[\s]+/.freeze
126
- TOKENS = Regexp.union(META, PARAM_NUMBER, NUMBER,
137
+ SEPARATORS = /\s+/.freeze
138
+ TOKENS = Regexp.union(META, PARAM_CONST, KEYWORD_NAME, PARAM_NUMBER, NUMBER,
127
139
  METHOD_NAME, SYMBOL, STRING)
128
140
 
129
141
  TOKEN = /\G(?:#{SEPARATORS}|#{TOKENS}|.)/.freeze
@@ -135,6 +147,8 @@ module RuboCop
135
147
  FUNCALL = /\A\##{METHOD_NAME}/.freeze
136
148
  LITERAL = /\A(?:#{SYMBOL}|#{NUMBER}|#{STRING})\Z/.freeze
137
149
  PARAM = /\A#{PARAM_NUMBER}\Z/.freeze
150
+ CONST = /\A#{PARAM_CONST}\Z/.freeze
151
+ KEYWORD = /\A#{KEYWORD_NAME}\Z/.freeze
138
152
  CLOSING = /\A(?:\)|\}|\])\Z/.freeze
139
153
 
140
154
  REST = '...'
@@ -193,6 +207,7 @@ module RuboCop
193
207
  @captures = 0 # number of captures seen
194
208
  @unify = {} # named wildcard -> temp variable
195
209
  @params = 0 # highest % (param) number seen
210
+ @keywords = Set[] # keyword parameters seen
196
211
  run(node_var)
197
212
  end
198
213
 
@@ -232,6 +247,8 @@ module RuboCop
232
247
  when LITERAL then compile_literal(token)
233
248
  when PREDICATE then compile_predicate(token)
234
249
  when NODE then compile_nodetype(token)
250
+ when KEYWORD then compile_keyword(token[1..-1])
251
+ when CONST then compile_const(token[1..-1])
235
252
  when PARAM then compile_param(token[1..-1])
236
253
  when CLOSING then fail_due_to("#{token} in invalid position")
237
254
  when nil then fail_due_to('pattern ended prematurely')
@@ -612,7 +629,15 @@ module RuboCop
612
629
  end
613
630
 
614
631
  def compile_param(number)
615
- "#{CUR_ELEMENT} == #{get_param(number)}"
632
+ "#{get_param(number)} === #{CUR_ELEMENT}"
633
+ end
634
+
635
+ def compile_const(const)
636
+ "#{get_const(const)} === #{CUR_ELEMENT}"
637
+ end
638
+
639
+ def compile_keyword(keyword)
640
+ "#{get_keyword(keyword)} === #{CUR_ELEMENT}"
616
641
  end
617
642
 
618
643
  def compile_args(tokens)
@@ -626,12 +651,14 @@ module RuboCop
626
651
  end
627
652
 
628
653
  def compile_arg(token)
654
+ name = token[1..-1]
629
655
  case token
630
- when WILDCARD then
631
- name = token[1..-1]
656
+ when WILDCARD
632
657
  access_unify(name) || fail_due_to('invalid in arglist: ' + token)
633
658
  when LITERAL then token
634
- when PARAM then get_param(token[1..-1])
659
+ when KEYWORD then get_keyword(name)
660
+ when CONST then get_const(name)
661
+ when PARAM then get_param(name)
635
662
  when CLOSING then fail_due_to("#{token} in invalid position")
636
663
  when nil then fail_due_to('pattern ended prematurely')
637
664
  else fail_due_to("invalid token in arglist: #{token.inspect}")
@@ -650,6 +677,15 @@ module RuboCop
650
677
  number.zero? ? @root : "param#{number}"
651
678
  end
652
679
 
680
+ def get_keyword(name)
681
+ @keywords << name
682
+ name
683
+ end
684
+
685
+ def get_const(const)
686
+ const # Output the constant exactly as given
687
+ end
688
+
653
689
  def emit_yield_capture(when_no_capture = '')
654
690
  yield_val = if @captures.zero?
655
691
  when_no_capture
@@ -675,9 +711,15 @@ module RuboCop
675
711
  (1..@params).map { |n| "param#{n}" }.join(',')
676
712
  end
677
713
 
678
- def emit_trailing_params
714
+ def emit_keyword_list(forwarding: false)
715
+ pattern = "%<keyword>s: #{'%<keyword>s' if forwarding}"
716
+ @keywords.map { |k| format(pattern, keyword: k) }.join(',')
717
+ end
718
+
719
+ def emit_trailing_params(forwarding: false)
679
720
  params = emit_param_list
680
- params.empty? ? '' : ",#{params}"
721
+ keywords = emit_keyword_list(forwarding: forwarding)
722
+ [params, keywords].reject(&:empty?).map { |p| ", #{p}" }.join
681
723
  end
682
724
 
683
725
  def emit_method_code
@@ -753,68 +795,51 @@ module RuboCop
753
795
  def self.tokens(pattern)
754
796
  pattern.scan(TOKEN).reject { |token| token =~ /\A#{SEPARATORS}\Z/ }
755
797
  end
756
- end
757
- private_constant :Compiler
758
-
759
- # Helpers for defining methods based on a pattern string
760
- module Macros
761
- # Define a method which applies a pattern to an AST node
762
- #
763
- # The new method will return nil if the node does not match
764
- # If the node matches, and a block is provided, the new method will
765
- # yield to the block (passing any captures as block arguments).
766
- # If the node matches, and no block is provided, the new method will
767
- # return the captures, or `true` if there were none.
768
- def def_node_matcher(method_name, pattern_str)
769
- compiler = Compiler.new(pattern_str, 'node')
770
- src = "def #{method_name}(node = self" \
771
- "#{compiler.emit_trailing_params});" \
772
- "#{compiler.emit_method_code};end"
773
-
774
- location = caller_locations(1, 1).first
775
- class_eval(src, location.path, location.lineno)
776
- end
777
798
 
778
- # Define a method which recurses over the descendants of an AST node,
779
- # checking whether any of them match the provided pattern
780
- #
781
- # If the method name ends with '?', the new method will return `true`
782
- # as soon as it finds a descendant which matches. Otherwise, it will
783
- # yield all descendants which match.
784
- def def_node_search(method_name, pattern_str)
785
- compiler = Compiler.new(pattern_str, 'node')
786
- called_from = caller(1..1).first.split(':')
787
-
788
- if method_name.to_s.end_with?('?')
789
- node_search_first(method_name, compiler, called_from)
790
- else
791
- node_search_all(method_name, compiler, called_from)
799
+ def def_helper(base, method_name, **defaults)
800
+ location = caller_locations(3, 1).first
801
+ unless defaults.empty?
802
+ base.send :define_method, method_name do |*args, **values|
803
+ send method_name, *args, **defaults, **values
804
+ end
805
+ method_name = :"without_defaults_#{method_name}"
792
806
  end
807
+ src = yield method_name
808
+ base.class_eval(src, location.path, location.lineno)
793
809
  end
794
810
 
795
- def node_search_first(method_name, compiler, called_from)
796
- node_search(method_name, compiler, 'return true', '', called_from)
811
+ def def_node_matcher(base, method_name, **defaults)
812
+ def_helper(base, method_name, **defaults) do |name|
813
+ <<~RUBY
814
+ def #{name}(node = self#{emit_trailing_params})
815
+ #{emit_method_code}
816
+ end
817
+ RUBY
818
+ end
797
819
  end
798
820
 
799
- def node_search_all(method_name, compiler, called_from)
800
- yield_code = compiler.emit_yield_capture('node')
801
- prelude = "return enum_for(:#{method_name}, node0" \
802
- "#{compiler.emit_trailing_params}) unless block_given?"
803
-
804
- node_search(method_name, compiler, yield_code, prelude, called_from)
821
+ def def_node_search(base, method_name, **defaults)
822
+ def_helper(base, method_name, **defaults) do |name|
823
+ emit_node_search(name)
824
+ end
805
825
  end
806
826
 
807
- def node_search(method_name, compiler, on_match, prelude, called_from)
808
- src = node_search_body(method_name, compiler.emit_trailing_params,
809
- prelude, compiler.match_code, on_match)
810
- filename, lineno = *called_from
811
- class_eval(src, filename, lineno.to_i)
827
+ def emit_node_search(method_name)
828
+ if method_name.to_s.end_with?('?')
829
+ on_match = 'return true'
830
+ else
831
+ prelude = <<~RUBY
832
+ return enum_for(:#{method_name},
833
+ node0#{emit_trailing_params(forwarding: true)}) unless block_given?
834
+ RUBY
835
+ on_match = emit_yield_capture('node')
836
+ end
837
+ emit_node_search_body(method_name, prelude: prelude, on_match: on_match)
812
838
  end
813
839
 
814
- def node_search_body(method_name, trailing_params, prelude, match_code,
815
- on_match)
840
+ def emit_node_search_body(method_name, prelude:, on_match:)
816
841
  <<~RUBY
817
- def #{method_name}(node0#{trailing_params})
842
+ def #{method_name}(node0#{emit_trailing_params})
818
843
  #{prelude}
819
844
  node0.each_node do |node|
820
845
  if #{match_code}
@@ -826,6 +851,33 @@ module RuboCop
826
851
  RUBY
827
852
  end
828
853
  end
854
+ private_constant :Compiler
855
+
856
+ # Helpers for defining methods based on a pattern string
857
+ module Macros
858
+ # Define a method which applies a pattern to an AST node
859
+ #
860
+ # The new method will return nil if the node does not match
861
+ # If the node matches, and a block is provided, the new method will
862
+ # yield to the block (passing any captures as block arguments).
863
+ # If the node matches, and no block is provided, the new method will
864
+ # return the captures, or `true` if there were none.
865
+ def def_node_matcher(method_name, pattern_str, **keyword_defaults)
866
+ Compiler.new(pattern_str, 'node')
867
+ .def_node_matcher(self, method_name, **keyword_defaults)
868
+ end
869
+
870
+ # Define a method which recurses over the descendants of an AST node,
871
+ # checking whether any of them match the provided pattern
872
+ #
873
+ # If the method name ends with '?', the new method will return `true`
874
+ # as soon as it finds a descendant which matches. Otherwise, it will
875
+ # yield all descendants which match.
876
+ def def_node_search(method_name, pattern_str, **keyword_defaults)
877
+ Compiler.new(pattern_str, 'node')
878
+ .def_node_search(self, method_name, **keyword_defaults)
879
+ end
880
+ end
829
881
 
830
882
  attr_reader :pattern
831
883
 
@@ -837,11 +889,15 @@ module RuboCop
837
889
  instance_eval(src, __FILE__, __LINE__ + 1)
838
890
  end
839
891
 
840
- def match(*args)
892
+ def match(*args, **rest)
841
893
  # If we're here, it's because the singleton method has not been defined,
842
894
  # either because we've been dup'ed or serialized through YAML
843
895
  initialize(pattern)
844
- match(*args)
896
+ if rest.empty?
897
+ match(*args)
898
+ else
899
+ match(*args, **rest)
900
+ end
845
901
  end
846
902
 
847
903
  def marshal_load(pattern)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'digest/sha1'
4
4
 
5
+ # rubocop:disable Metrics/ClassLength
5
6
  module RuboCop
6
7
  module AST
7
8
  # ProcessedSource contains objects which are generated by Parser
@@ -176,6 +177,9 @@ module RuboCop
176
177
  when 2.7
177
178
  require 'parser/ruby27'
178
179
  Parser::Ruby27
180
+ when 2.8
181
+ require 'parser/ruby28'
182
+ Parser::Ruby28
179
183
  else
180
184
  raise ArgumentError,
181
185
  "RuboCop found unknown Ruby version: #{ruby_version.inspect}"
@@ -201,3 +205,4 @@ module RuboCop
201
205
  end
202
206
  end
203
207
  end
208
+ # rubocop:enable Metrics/ClassLength
@@ -19,9 +19,10 @@ module RuboCop
19
19
  rational str sym regopt self lvar
20
20
  ivar cvar gvar nth_ref back_ref cbase
21
21
  arg restarg blockarg shadowarg
22
- kwrestarg zsuper lambda redo retry
22
+ kwrestarg zsuper redo retry
23
23
  forward_args forwarded_args
24
- match_var match_nil_pattern empty_else].freeze
24
+ match_var match_nil_pattern empty_else
25
+ forward_arg lambda procarg0 __ENCODING__].freeze
25
26
  ONE_CHILD_NODE = %i[splat kwsplat block_pass not break next
26
27
  preexe postexe match_current_line defined?
27
28
  arg_expr pin match_rest if_guard unless_guard
@@ -33,7 +34,8 @@ module RuboCop
33
34
  match_with_lvasgn begin kwbegin return
34
35
  in_match match_alt
35
36
  match_as array_pattern array_pattern_with_tail
36
- hash_pattern const_pattern].freeze
37
+ hash_pattern const_pattern
38
+ index indexasgn].freeze
37
39
  SECOND_CHILD_ONLY = %i[lvasgn ivasgn cvasgn gvasgn optarg kwarg
38
40
  kwoptarg].freeze
39
41
 
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module AST
5
5
  module Version
6
- STRING = '0.0.3'
6
+ STRING = '0.1.0'
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-ast
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bozhidar Batsov
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-05-15 00:00:00.000000000 Z
13
+ date: 2020-06-26 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: parser
@@ -77,8 +77,11 @@ files:
77
77
  - lib/rubocop/ast/node/forward_args_node.rb
78
78
  - lib/rubocop/ast/node/hash_node.rb
79
79
  - lib/rubocop/ast/node/if_node.rb
80
+ - lib/rubocop/ast/node/index_node.rb
81
+ - lib/rubocop/ast/node/indexasgn_node.rb
80
82
  - lib/rubocop/ast/node/int_node.rb
81
83
  - lib/rubocop/ast/node/keyword_splat_node.rb
84
+ - lib/rubocop/ast/node/lambda_node.rb
82
85
  - lib/rubocop/ast/node/mixin/basic_literal_node.rb
83
86
  - lib/rubocop/ast/node/mixin/binary_operator_node.rb
84
87
  - lib/rubocop/ast/node/mixin/collection_node.rb
@@ -120,7 +123,7 @@ metadata:
120
123
  homepage_uri: https://www.rubocop.org/
121
124
  changelog_uri: https://github.com/rubocop-hq/rubocop-ast/blob/master/CHANGELOG.md
122
125
  source_code_uri: https://github.com/rubocop-hq/rubocop-ast/
123
- documentation_uri: https://docs.rubocop.org/
126
+ documentation_uri: https://docs.rubocop.org/rubocop-ast/
124
127
  bug_tracker_uri: https://github.com/rubocop-hq/rubocop-ast/issues
125
128
  post_install_message:
126
129
  rdoc_options: []