rubocop 0.34.2 → 0.35.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rubocop might be problematic. Click here for more details.

Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -0
  3. data/README.md +103 -31
  4. data/config/default.yml +32 -2
  5. data/config/disabled.yml +24 -0
  6. data/config/enabled.yml +20 -2
  7. data/lib/rubocop.rb +13 -0
  8. data/lib/rubocop/ast_node.rb +48 -0
  9. data/lib/rubocop/cli.rb +9 -0
  10. data/lib/rubocop/config.rb +8 -6
  11. data/lib/rubocop/config_loader.rb +30 -8
  12. data/lib/rubocop/cop/commissioner.rb +1 -1
  13. data/lib/rubocop/cop/cop.rb +19 -6
  14. data/lib/rubocop/cop/lint/circular_argument_reference.rb +33 -2
  15. data/lib/rubocop/cop/lint/debugger.rb +9 -56
  16. data/lib/rubocop/cop/lint/end_alignment.rb +29 -9
  17. data/lib/rubocop/cop/lint/eval.rb +6 -2
  18. data/lib/rubocop/cop/lint/format_parameter_mismatch.rb +24 -6
  19. data/lib/rubocop/cop/lint/literal_in_condition.rb +0 -5
  20. data/lib/rubocop/cop/lint/non_local_exit_from_iterator.rb +10 -1
  21. data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +1 -1
  22. data/lib/rubocop/cop/lint/space_before_first_arg.rb +1 -1
  23. data/lib/rubocop/cop/lint/unused_block_argument.rb +6 -0
  24. data/lib/rubocop/cop/lint/unused_method_argument.rb +8 -0
  25. data/lib/rubocop/cop/metrics/abc_size.rb +1 -1
  26. data/lib/rubocop/cop/mixin/access_modifier_node.rb +1 -1
  27. data/lib/rubocop/cop/mixin/autocorrect_alignment.rb +1 -1
  28. data/lib/rubocop/cop/mixin/autocorrect_unless_changing_ast.rb +26 -3
  29. data/lib/rubocop/cop/mixin/check_assignment.rb +2 -3
  30. data/lib/rubocop/cop/mixin/configurable_enforced_style.rb +59 -12
  31. data/lib/rubocop/cop/mixin/configurable_max.rb +1 -1
  32. data/lib/rubocop/cop/mixin/configurable_naming.rb +1 -1
  33. data/lib/rubocop/cop/mixin/first_element_line_break.rb +41 -0
  34. data/lib/rubocop/cop/mixin/negative_conditional.rb +1 -1
  35. data/lib/rubocop/cop/mixin/safe_assignment.rb +3 -14
  36. data/lib/rubocop/cop/mixin/statement_modifier.rb +2 -2
  37. data/lib/rubocop/cop/performance/detect.rb +5 -1
  38. data/lib/rubocop/cop/performance/fixed_size.rb +50 -0
  39. data/lib/rubocop/cop/performance/size.rb +1 -1
  40. data/lib/rubocop/cop/performance/string_replacement.rb +14 -8
  41. data/lib/rubocop/cop/rails/pluralization_grammar.rb +97 -0
  42. data/lib/rubocop/cop/style/align_hash.rb +1 -12
  43. data/lib/rubocop/cop/style/align_parameters.rb +19 -7
  44. data/lib/rubocop/cop/style/and_or.rb +42 -13
  45. data/lib/rubocop/cop/style/block_comments.rb +4 -2
  46. data/lib/rubocop/cop/style/block_delimiters.rb +57 -18
  47. data/lib/rubocop/cop/style/braces_around_hash_parameters.rb +1 -1
  48. data/lib/rubocop/cop/style/command_literal.rb +2 -10
  49. data/lib/rubocop/cop/style/copyright.rb +5 -3
  50. data/lib/rubocop/cop/style/documentation.rb +9 -6
  51. data/lib/rubocop/cop/style/dot_position.rb +6 -0
  52. data/lib/rubocop/cop/style/double_negation.rb +4 -15
  53. data/lib/rubocop/cop/style/each_with_object.rb +17 -4
  54. data/lib/rubocop/cop/style/empty_line_between_defs.rb +1 -5
  55. data/lib/rubocop/cop/style/encoding.rb +10 -4
  56. data/lib/rubocop/cop/style/extra_spacing.rb +23 -13
  57. data/lib/rubocop/cop/style/first_array_element_line_break.rb +41 -0
  58. data/lib/rubocop/cop/style/first_hash_element_line_break.rb +35 -0
  59. data/lib/rubocop/cop/style/first_method_argument_line_break.rb +37 -0
  60. data/lib/rubocop/cop/style/first_method_parameter_line_break.rb +42 -0
  61. data/lib/rubocop/cop/style/for.rb +2 -1
  62. data/lib/rubocop/cop/style/if_unless_modifier.rb +31 -0
  63. data/lib/rubocop/cop/style/indent_hash.rb +67 -37
  64. data/lib/rubocop/cop/style/indentation_width.rb +1 -1
  65. data/lib/rubocop/cop/style/leading_comment_space.rb +3 -2
  66. data/lib/rubocop/cop/style/method_call_parentheses.rb +8 -0
  67. data/lib/rubocop/cop/style/method_def_parentheses.rb +10 -7
  68. data/lib/rubocop/cop/style/multiline_operation_indentation.rb +8 -13
  69. data/lib/rubocop/cop/style/nested_modifier.rb +97 -0
  70. data/lib/rubocop/cop/style/next.rb +18 -0
  71. data/lib/rubocop/cop/style/parallel_assignment.rb +57 -15
  72. data/lib/rubocop/cop/style/predicate_name.rb +7 -2
  73. data/lib/rubocop/cop/style/regexp_literal.rb +2 -10
  74. data/lib/rubocop/cop/style/single_line_methods.rb +7 -5
  75. data/lib/rubocop/cop/style/single_space_before_first_arg.rb +1 -1
  76. data/lib/rubocop/cop/style/space_around_operators.rb +2 -0
  77. data/lib/rubocop/cop/style/special_global_vars.rb +4 -2
  78. data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +108 -0
  79. data/lib/rubocop/cop/style/trailing_comma.rb +9 -6
  80. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +23 -2
  81. data/lib/rubocop/cop/style/unneeded_percent_q.rb +31 -20
  82. data/lib/rubocop/cop/style/variable_name.rb +5 -0
  83. data/lib/rubocop/cop/style/word_array.rb +2 -1
  84. data/lib/rubocop/cop/team.rb +17 -4
  85. data/lib/rubocop/cop/util.rb +5 -0
  86. data/lib/rubocop/cop/variable_force/locatable.rb +1 -1
  87. data/lib/rubocop/formatter/base_formatter.rb +1 -1
  88. data/lib/rubocop/formatter/disabled_config_formatter.rb +22 -10
  89. data/lib/rubocop/formatter/simple_text_formatter.rb +1 -1
  90. data/lib/rubocop/node_pattern.rb +390 -0
  91. data/lib/rubocop/options.rb +48 -36
  92. data/lib/rubocop/processed_source.rb +3 -1
  93. data/lib/rubocop/rake_task.rb +1 -1
  94. data/lib/rubocop/remote_config.rb +60 -0
  95. data/lib/rubocop/result_cache.rb +4 -2
  96. data/lib/rubocop/runner.rb +33 -10
  97. data/lib/rubocop/token.rb +2 -1
  98. data/lib/rubocop/version.rb +1 -1
  99. data/lib/rubocop/warning.rb +11 -0
  100. data/relnotes/v0.35.0.md +210 -0
  101. data/rubocop.gemspec +2 -2
  102. metadata +20 -6
@@ -68,7 +68,6 @@ module RuboCop
68
68
  node.loc.end.begin_pos)
69
69
  end
70
70
 
71
- # rubocop:disable Metrics/MethodLength
72
71
  def check(node, items, kind, begin_pos, end_pos)
73
72
  sb = items.first.loc.expression.source_buffer
74
73
 
@@ -77,10 +76,9 @@ module RuboCop
77
76
  return if heredoc?(after_last_item.source)
78
77
 
79
78
  comma_offset = after_last_item.source =~ /,/
80
- should_have_comma =
81
- [:comma, :consistent_comma].include?(style) && multiline?(node)
79
+
82
80
  if comma_offset && !inside_comment?(after_last_item, comma_offset)
83
- unless should_have_comma
81
+ unless should_have_comma?(style, node, items)
84
82
  extra_info = case style
85
83
  when :comma
86
84
  ', unless each item is on its own line'
@@ -92,11 +90,16 @@ module RuboCop
92
90
  avoid_comma(kind, after_last_item.begin_pos + comma_offset, sb,
93
91
  extra_info)
94
92
  end
95
- elsif should_have_comma
93
+ elsif should_have_comma?(style, node, items)
96
94
  put_comma(items, kind, sb)
97
95
  end
98
96
  end
99
- # rubocop:enable Metrics/MethodLength
97
+
98
+ def should_have_comma?(style, node, items)
99
+ [:comma, :consistent_comma].include?(style) &&
100
+ multiline?(node) &&
101
+ (items.size > 1 || items.last.hash_type?)
102
+ end
100
103
 
101
104
  def inside_comment?(range, comma_offset)
102
105
  processed_source.comments.any? do |comment|
@@ -15,10 +15,13 @@ module RuboCop
15
15
  # #good
16
16
  # a, b, = foo()
17
17
  # a, = foo()
18
+ # *a, b, _ = foo() => We need to know to not include 2 variables in a
19
+ # a, *b, _ = foo() => The correction `a, *b, = foo()` is a syntax error
18
20
  class TrailingUnderscoreVariable < Cop
19
21
  include SurroundingSpace
20
22
 
21
- MSG = 'Do not use trailing `_`s in parallel assignment.'
23
+ MSG = 'Do not use trailing `_`s in parallel assignment.'.freeze
24
+ UNDERSCORE = '_'.freeze
22
25
 
23
26
  def on_masgn(node)
24
27
  left, = *node
@@ -60,12 +63,30 @@ module RuboCop
60
63
  first_offense = nil
61
64
 
62
65
  variables.reverse_each do |variable|
63
- break unless variable.children.first == :_
66
+ var, = *variable
67
+ var, = *var
68
+ if allow_named_underscore_variables
69
+ break unless var == :_
70
+ else
71
+ break unless var.to_s.start_with?(UNDERSCORE)
72
+ end
64
73
  first_offense = variable
65
74
  end
66
75
 
76
+ return nil if first_offense.nil?
77
+
78
+ first_offense_index = variables.index(first_offense)
79
+ 0.upto(first_offense_index - 1).each do |index|
80
+ return nil if variables[index].splat_type?
81
+ end
82
+
67
83
  first_offense
68
84
  end
85
+
86
+ def allow_named_underscore_variables
87
+ @allow_named_underscore_variables ||=
88
+ cop_config['AllowNamedUnderscoreVariables']
89
+ end
69
90
  end
70
91
  end
71
92
  end
@@ -7,51 +7,62 @@ module RuboCop
7
7
  class UnneededPercentQ < Cop
8
8
  MSG = 'Use `%s` only for strings that contain both single quotes and ' \
9
9
  'double quotes%s.'
10
+ DYNAMIC_MSG = ', or for dynamic strings that contain double quotes'
11
+ SINGLE_QUOTE = "'".freeze
12
+ QUOTE = '"'.freeze
13
+ EMPTY = ''.freeze
14
+ PERCENT_Q = '%q'.freeze
15
+ PERCENT_CAPITAL_Q = '%Q'.freeze
16
+ STRING_INTERPOLATION_REGEXP = /#\{.+}/
10
17
 
11
18
  def on_dstr(node)
12
- # Using %Q to avoid escaping inner " is ok.
13
- check(node) unless node.loc.expression.source =~ /"/
19
+ check(node)
14
20
  end
15
21
 
16
22
  def on_str(node)
23
+ # Interpolated strings that contain more than just interpolation
24
+ # will call `on_dstr` for the entire string and `on_str` for the
25
+ # non interpolated portion of the string
26
+ return unless string_literal?(node)
17
27
  check(node)
18
28
  end
19
29
 
20
- # We process regexp nodes because the inner str nodes can cause
21
- # confusion in on_str if they start with %( or %Q(.
22
- def on_regexp(node)
23
- string_parts = node.children.select { |child| child.type == :str }
24
- string_parts.each { |s| ignore_node(s) }
25
- end
26
-
27
30
  private
28
31
 
29
32
  def check(node)
30
- if node.loc.respond_to?(:heredoc_body)
31
- ignore_node(node)
32
- return
33
- end
34
- return if ignored_node?(node) || part_of_ignored_node?(node)
35
33
  src = node.loc.expression.source
36
- return unless src.start_with?('%q') || src.start_with?('%Q')
37
- return if src =~ /'/ && src =~ /"/
34
+ return unless start_with_percent_q_variant?(src)
35
+ return if src.include?(SINGLE_QUOTE) && src.include?(QUOTE)
38
36
  return if src =~ StringHelp::ESCAPED_CHAR_REGEXP
37
+ if src.start_with?(PERCENT_Q) && src =~ STRING_INTERPOLATION_REGEXP
38
+ return
39
+ end
39
40
 
40
- extra = if src.start_with?('%Q')
41
- ', or for dynamic strings that contain double quotes'
41
+ extra = if src.start_with?(PERCENT_CAPITAL_Q)
42
+ DYNAMIC_MSG
42
43
  else
43
- ''
44
+ EMPTY
44
45
  end
45
46
  add_offense(node, :expression, format(MSG, src[0, 2], extra))
46
47
  end
47
48
 
48
49
  def autocorrect(node)
49
- delimiter = node.loc.expression.source =~ /^%Q[^"]+$|'/ ? '"' : "'"
50
+ delimiter =
51
+ node.loc.expression.source =~ /^%Q[^"]+$|'/ ? QUOTE : SINGLE_QUOTE
50
52
  lambda do |corrector|
51
53
  corrector.replace(node.loc.begin, delimiter)
52
54
  corrector.replace(node.loc.end, delimiter)
53
55
  end
54
56
  end
57
+
58
+ def string_literal?(node)
59
+ node.loc.respond_to?(:begin) && node.loc.respond_to?(:end) &&
60
+ node.loc.begin && node.loc.end
61
+ end
62
+
63
+ def start_with_percent_q_variant?(string)
64
+ string.start_with?(PERCENT_Q) || string.start_with?(PERCENT_CAPITAL_Q)
65
+ end
55
66
  end
56
67
  end
57
68
  end
@@ -23,6 +23,11 @@ module RuboCop
23
23
  check_name(node, name, node.loc.name)
24
24
  end
25
25
 
26
+ def on_arg(node)
27
+ name, = *node
28
+ check_name(node, name, node.loc.name)
29
+ end
30
+
26
31
  private
27
32
 
28
33
  def message(style)
@@ -13,6 +13,7 @@ module RuboCop
13
13
  include ConfigurableMax
14
14
 
15
15
  MSG = 'Use `%w` or `%W` for array of words.'
16
+ QUESTION_MARK_SIZE = '?'.size
16
17
 
17
18
  def on_array(node)
18
19
  array_elems = node.children
@@ -84,7 +85,7 @@ module RuboCop
84
85
  def source_for(str_node)
85
86
  if character_literal?(str_node)
86
87
  @interpolated = true
87
- begin_pos = str_node.loc.expression.begin_pos + '?'.length
88
+ begin_pos = str_node.loc.expression.begin_pos + QUESTION_MARK_SIZE
88
89
  end_pos = str_node.loc.expression.end_pos
89
90
  else
90
91
  begin_pos = str_node.loc.begin.end_pos
@@ -4,7 +4,7 @@ module RuboCop
4
4
  module Cop
5
5
  # FIXME
6
6
  class Team
7
- attr_reader :errors, :updated_source_file
7
+ attr_reader :errors, :warnings, :updated_source_file
8
8
 
9
9
  alias_method :updated_source_file?, :updated_source_file
10
10
 
@@ -13,6 +13,7 @@ module RuboCop
13
13
  @config = config
14
14
  @options = options || { auto_correct: false, debug: false }
15
15
  @errors = []
16
+ @warnings = []
16
17
  end
17
18
 
18
19
  def autocorrect?
@@ -96,13 +97,25 @@ module RuboCop
96
97
  def process_commissioner_errors(file, file_errors)
97
98
  file_errors.each do |cop, errors|
98
99
  errors.each do |e|
99
- handle_error(e,
100
- Rainbow("An error occurred while #{cop.name}" \
101
- " cop was inspecting #{file}.").red)
100
+ if e.is_a?(Warning)
101
+ handle_warning(e,
102
+ Rainbow("#{e.message} (from file: " \
103
+ "#{file})").yellow)
104
+ else
105
+ handle_error(e,
106
+ Rainbow("An error occurred while #{cop.name}" \
107
+ " cop was inspecting #{file}.").red)
108
+ end
102
109
  end
103
110
  end
104
111
  end
105
112
 
113
+ def handle_warning(e, message)
114
+ @warnings << message
115
+ warn message
116
+ puts e.backtrace if debug?
117
+ end
118
+
106
119
  def handle_error(e, message)
107
120
  @errors << message
108
121
  warn message
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
+ # rubocop:disable Metrics/ModuleLength
3
4
  module RuboCop
4
5
  module Cop
5
6
  # This module contains a collection of useful utility methods.
@@ -12,6 +13,10 @@ module RuboCop
12
13
  SHORTHAND_ASGN_NODES = [:op_asgn, :or_asgn, :and_asgn]
13
14
  ASGN_NODES = EQUALS_ASGN_NODES + SHORTHAND_ASGN_NODES
14
15
 
16
+ LITERALS = [:str, :dstr, :int, :float, :sym, :dsym, :array,
17
+ :hash, :regexp, :nil, :true, :false]
18
+ BASIC_LITERALS = LITERALS - [:dstr, :dsym, :array, :hash]
19
+
15
20
  # http://phrogz.net/programmingruby/language.html#table_18.4
16
21
  # Backtick is added last just to help editors parse this code.
17
22
  OPERATOR_METHODS = %w(
@@ -133,7 +133,7 @@ module RuboCop
133
133
  end
134
134
 
135
135
  def body_index
136
- branch_point_node.children.index(branch_body_node)
136
+ branch_point_node.children.index { |n| n.equal?(branch_body_node) }
137
137
  end
138
138
 
139
139
  def set_branch_point_and_body_nodes!
@@ -1,6 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
- # rubocop:disable Metrics/LineLength, Lint/UnusedMethodArgument
3
+ # rubocop:disable Metrics/LineLength
4
4
 
5
5
  module RuboCop
6
6
  module Formatter
@@ -33,11 +33,11 @@ module RuboCop
33
33
 
34
34
  def file_finished(file, offenses)
35
35
  @cops_with_offenses ||= Hash.new(0)
36
- @files_with_offences ||= {}
36
+ @files_with_offenses ||= {}
37
37
  offenses.each do |o|
38
38
  @cops_with_offenses[o.cop_name] += 1
39
- @files_with_offences[o.cop_name] ||= []
40
- @files_with_offences[o.cop_name] << file
39
+ @files_with_offenses[o.cop_name] ||= []
40
+ @files_with_offenses[o.cop_name] << file
41
41
  end
42
42
  end
43
43
 
@@ -56,9 +56,7 @@ module RuboCop
56
56
  cfg = self.class.config_to_allow_offenses[cop_name]
57
57
  cfg ||= {}
58
58
  output_cop_comments(output, cfg, cop_name, offense_count)
59
- output.puts "#{cop_name}:"
60
- cfg.each { |key, value| output.puts " #{key}: #{value}" }
61
- output_offending_files(output, cfg, cop_name)
59
+ output_cop_config(output, cfg, cop_name)
62
60
  end
63
61
  puts "Created #{output.path}."
64
62
  puts "Run `rubocop --config #{output.path}`, or"
@@ -82,23 +80,37 @@ module RuboCop
82
80
  output.puts "# Configuration parameters: #{params.join(', ')}."
83
81
  end
84
82
 
83
+ def output_cop_config(output, cfg, cop_name)
84
+ output.puts "#{cop_name}:"
85
+ cfg.each do |key, value|
86
+ value = value[0] if value.is_a?(Array)
87
+ output.puts " #{key}: #{value}"
88
+ end
89
+ output_offending_files(output, cfg, cop_name)
90
+ end
91
+
85
92
  def output_offending_files(output, cfg, cop_name)
86
93
  return unless cfg.empty?
87
94
 
88
- offending_files = @files_with_offences[cop_name].uniq.sort
95
+ offending_files = @files_with_offenses[cop_name].uniq.sort
89
96
  if offending_files.count > @exclude_limit
90
97
  output.puts ' Enabled: false'
91
98
  else
92
- output_exclude_list(output, offending_files)
99
+ output_exclude_list(output, offending_files, cop_name)
93
100
  end
94
101
  end
95
102
 
96
- def output_exclude_list(output, offending_files)
103
+ def output_exclude_list(output, offending_files, cop_name)
97
104
  require 'pathname'
98
105
  parent = Pathname.new(Dir.pwd)
99
106
 
107
+ # Exclude properties in .rubocop_todo.yml override default ones, so in
108
+ # order to retain the default excludes we must copy them.
109
+ default_cfg = RuboCop::ConfigLoader.default_configuration[cop_name]
110
+ default_excludes = default_cfg ? default_cfg['Exclude'] || [] : []
111
+
100
112
  output.puts ' Exclude:'
101
- offending_files.each do |file|
113
+ (default_excludes + offending_files).each do |file|
102
114
  file_path = Pathname.new(file)
103
115
  begin
104
116
  relative = file_path.relative_path_from(parent)
@@ -90,7 +90,7 @@ module RuboCop
90
90
  end
91
91
 
92
92
  def annotate_message(msg)
93
- msg.gsub(/`(.*?)`/, Rainbow('\1').yellow)
93
+ msg.gsub(/`(.*?)`/, yellow('\1'))
94
94
  end
95
95
 
96
96
  def message(offense)
@@ -0,0 +1,390 @@
1
+ # encoding: utf-8
2
+
3
+ # rubocop:disable Metrics/ClassLength
4
+ # rubocop:disable Metrics/CyclomaticComplexity
5
+
6
+ module RuboCop
7
+ # This class performs a pattern-matching operation on an AST node.
8
+ #
9
+ # Initialize a new `NodePattern` with `NodePattern.new(pattern_string)`, then
10
+ # pass an AST node to `NodePattern#match`. Alternatively, use one of the class
11
+ # macros in `NodePattern::Macros` to define your own pattern-matching method.
12
+ #
13
+ # If the match fails, `nil` will be returned. If the match succeeds, the
14
+ # return value depends on whether a block was provided to `#match`, and
15
+ # whether the pattern contained any "captures" (values which are extracted
16
+ # from a matching AST.)
17
+ #
18
+ # - With block: #match yields the captures (if any) and passes the return
19
+ # value of the block through.
20
+ # - With no block, but one capture: the capture is returned.
21
+ # - With no block, but multiple captures: captures are returned as an array.
22
+ # - With no captures: #match returns `true`.
23
+ #
24
+ # ## Pattern string format examples
25
+ #
26
+ # ':sym' # matches a literal symbol
27
+ # '1' # matches a literal integer
28
+ # 'nil' # matches a literal nil
29
+ # 'send' # matches (send ...)
30
+ # '(send)' # matches (send)
31
+ # '(send ...)' # matches (send ...)
32
+ # '{send class}' # matches (send ...) or (class ...)
33
+ # '({send class})' # matches (send) or (class)
34
+ # '(send const)' # matches (send (const ...))
35
+ # '(send _ :new)' # matches (send <anything> :new)
36
+ # '(send $_ :new)' # as above, but whatever matches the $_ is captured
37
+ # '(send $_ $_)' # you can use as many captures as you want
38
+ # '(send !const ...)' # ! negates the next part of the pattern
39
+ # '$(send const ...)' # arbitrary matching can be performed on a capture
40
+ # '(send _recv _msg)' # wildcards can be named (for readability)
41
+ # '(send ... :new)' # you can specifically match against the last child
42
+ # # (this only works for the very last)
43
+ # '(send $...)' # capture all the children as an array
44
+ # '(send $... int)' # capture all children but the last as an array
45
+ # '(send _x :+ _x)' # unification is performed on named wildcards
46
+ # # (like Prolog variables...)
47
+ # '(int odd?)' # words which end with a ? are predicate methods,
48
+ # # are are called on the target to see if it matches
49
+ # # any Ruby method which the matched object supports
50
+ # # can be used
51
+ # '(int [!1 !2])' # [] contains multiple patterns, ALL of which must
52
+ # # match in that position
53
+ # # ({} is pattern union, [] is intersection)
54
+ # '(send %1 _)' # % stands for a parameter which must be supplied to
55
+ # # #match at matching time
56
+ # # it will be compared to the corresponding value in
57
+ # # the AST using #==
58
+ # # a bare '%' is the same as '%1'
59
+ # '^^send' # each ^ ascends one level in the AST
60
+ # # so this matches against the grandparent node
61
+ #
62
+ # You can nest arbitrarily deep:
63
+ #
64
+ # # matches node parsed from 'Const = Class.new' or 'Const = Module.new':
65
+ # '(casgn nil const (send (const nil {:Class :Module}) :new)))'
66
+ # # matches a node parsed from an 'if', with a '==' comparison,
67
+ # # and no 'else' branch:
68
+ # '(if (send _ :== _) _ nil)'
69
+ #
70
+ # Note that patterns like 'send' are implemented by calling `#send_type?` on
71
+ # the node being matched, 'const' by `#const_type?`, 'int' by `#int_type?`,
72
+ # and so on. Therefore, if you add methods which are named like
73
+ # `#prefix_type?` to the AST node class, then 'prefix' will become usable as
74
+ # a pattern.
75
+ #
76
+ # Also node that if you need a "guard clause" to protect against possible nils
77
+ # in a certain place in the AST, you can do it like this: `[!nil <pattern>]`
78
+ #
79
+ class NodePattern
80
+ # @private
81
+ Invalid = Class.new(StandardError)
82
+
83
+ # @private
84
+ # Builds Ruby code which implements a pattern
85
+ class Compiler
86
+ RSYM = %r{:(?:[\w+-@_*/?!<>~|%^]+|==|\[\]=?)}
87
+ ID_CHAR = /[a-zA-Z_]/
88
+ META_CHAR = /\(|\)|\{|\}|\[|\]|\$\.\.\.|\$|!|\^|\.\.\./
89
+ TOKEN = /\G(?:\s+|#{META_CHAR}|#{ID_CHAR}+\??|%\d*|\d+|#{RSYM}|.)/
90
+
91
+ NODE = /\A#{ID_CHAR}+\Z/
92
+ PREDICATE = /\A#{ID_CHAR}+\?\Z/
93
+ LITERAL = /\A(?:#{RSYM}|\d+|nil)\Z/
94
+ WILDCARD = /\A_#{ID_CHAR}*\Z/
95
+ PARAM = /\A%\d*\Z/
96
+ CLOSING = /\A(?:\)|\}|\])\Z/
97
+
98
+ attr_reader :match_code
99
+
100
+ def initialize(str, node_var = 'node0')
101
+ @string = str
102
+
103
+ @temps = 0 # avoid name clashes between temp variables
104
+ @captures = 0 # number of captures seen
105
+ @unify = {} # named wildcard -> temp variable number
106
+ @params = 0 # highest % (param) number seen
107
+
108
+ run(node_var)
109
+ end
110
+
111
+ def run(node_var)
112
+ tokens = @string.scan(TOKEN)
113
+ tokens.reject! { |token| token =~ /\A\s+\Z/ }
114
+ @match_code = compile_expr(tokens, node_var, false)
115
+ fail_due_to('unbalanced pattern') unless tokens.empty?
116
+ end
117
+
118
+ def compile_expr(tokens, cur_node, seq_head)
119
+ token = tokens.shift
120
+ case token
121
+ when '(' then compile_seq(tokens, cur_node, seq_head)
122
+ when '{' then compile_union(tokens, cur_node, seq_head)
123
+ when '[' then compile_intersect(tokens, cur_node, seq_head)
124
+ when '!' then compile_negation(tokens, cur_node, seq_head)
125
+ when '$' then compile_capture(tokens, cur_node, seq_head)
126
+ when '^' then compile_ascend(tokens, cur_node, seq_head)
127
+ when WILDCARD then compile_wildcard(cur_node, token[1..-1], seq_head)
128
+ when LITERAL then compile_literal(cur_node, token, seq_head)
129
+ when PREDICATE then compile_predicate(cur_node, token, seq_head)
130
+ when NODE then compile_nodetype(cur_node, token)
131
+ when PARAM then compile_param(cur_node, token[1..-1], seq_head)
132
+ when CLOSING then fail_due_to("#{token} in invalid position")
133
+ when nil then fail_due_to('pattern ended prematurely')
134
+ else fail_due_to("invalid token #{token.inspect}")
135
+ end
136
+ end
137
+
138
+ def compile_seq(tokens, cur_node, seq_head)
139
+ fail_due_to('empty parentheses') if tokens.first == ')'
140
+ fail_due_to('parentheses at sequence head') if seq_head
141
+
142
+ init = "temp#{@temps += 1} = #{cur_node}"
143
+ cur_node = "temp#{@temps}"
144
+ terms = compile_seq_terms(tokens, cur_node)
145
+
146
+ join_terms(init, terms, ' && ')
147
+ end
148
+
149
+ def compile_seq_terms(tokens, cur_node)
150
+ terms = []
151
+ index = nil
152
+ until tokens.first == ')'
153
+ if tokens.first == '...'
154
+ return compile_ellipsis(tokens, cur_node, terms, index || 0)
155
+ elsif tokens.first == '$...'
156
+ return compile_capt_ellip(tokens, cur_node, terms, index || 0)
157
+ elsif index.nil?
158
+ terms << compile_expr(tokens, cur_node, true)
159
+ index = 0
160
+ else
161
+ child_node = "#{cur_node}.children[#{index}]"
162
+ terms << compile_expr(tokens, child_node, false)
163
+ index += 1
164
+ end
165
+ end
166
+ terms << "(#{cur_node}.children.size == #{index})"
167
+ tokens.shift # drop concluding )
168
+ terms
169
+ end
170
+
171
+ def compile_ellipsis(tokens, cur_node, terms, index)
172
+ if (term = compile_seq_tail(tokens, "#{cur_node}.children.last"))
173
+ terms << "(#{cur_node}.children.size > #{index})"
174
+ terms << term
175
+ elsif index > 0
176
+ terms << "(#{cur_node}.children.size >= #{index})"
177
+ end
178
+ terms
179
+ end
180
+
181
+ def compile_capt_ellip(tokens, cur_node, terms, index)
182
+ capture = next_capture
183
+ if (term = compile_seq_tail(tokens, "#{cur_node}.children.last"))
184
+ terms << "(#{cur_node}.children.size > #{index})"
185
+ terms << term
186
+ terms << "(#{capture} = #{cur_node}.children[#{index}..-2])"
187
+ else
188
+ terms << "(#{cur_node}.children.size >= #{index})" if index > 0
189
+ terms << "(#{capture} = #{cur_node}.children[#{index}..-1])"
190
+ end
191
+ terms
192
+ end
193
+
194
+ def compile_seq_tail(tokens, cur_node)
195
+ tokens.shift
196
+ if tokens.first == ')'
197
+ tokens.shift
198
+ nil
199
+ else
200
+ expr = compile_expr(tokens, cur_node, false)
201
+ fail_due_to('missing )') unless tokens.shift == ')'
202
+ expr
203
+ end
204
+ end
205
+
206
+ def compile_union(tokens, cur_node, seq_head)
207
+ fail_due_to('empty union') if tokens.first == '}'
208
+
209
+ init = "temp#{@temps += 1} = #{cur_node}"
210
+ cur_node = "temp#{@temps}"
211
+
212
+ terms = []
213
+ captures_before = @captures
214
+ terms << compile_expr(tokens, cur_node, seq_head)
215
+ captures_after = @captures
216
+
217
+ until tokens.first == '}'
218
+ @captures = captures_before
219
+ terms << compile_expr(tokens, cur_node, seq_head)
220
+ if @captures != captures_after
221
+ fail_due_to('each branch of {} must have same # of captures')
222
+ end
223
+ end
224
+ tokens.shift
225
+
226
+ join_terms(init, terms, ' || ')
227
+ end
228
+
229
+ def compile_intersect(tokens, cur_node, seq_head)
230
+ fail_due_to('empty intersection') if tokens.first == ']'
231
+
232
+ init = "temp#{@temps += 1} = #{cur_node}"
233
+ cur_node = "temp#{@temps}"
234
+
235
+ terms = []
236
+ until tokens.first == ']'
237
+ terms << compile_expr(tokens, cur_node, seq_head)
238
+ end
239
+ tokens.shift
240
+
241
+ join_terms(init, terms, ' && ')
242
+ end
243
+
244
+ def compile_capture(tokens, cur_node, seq_head)
245
+ "(#{next_capture} = #{cur_node}#{'.type' if seq_head}; " <<
246
+ compile_expr(tokens, cur_node, seq_head) <<
247
+ ')'
248
+ end
249
+
250
+ def compile_negation(tokens, cur_node, seq_head)
251
+ '(!' << compile_expr(tokens, cur_node, seq_head) << ')'
252
+ end
253
+
254
+ def compile_ascend(tokens, cur_node, seq_head)
255
+ "(#{cur_node}.parent && " <<
256
+ compile_expr(tokens, "#{cur_node}.parent", seq_head) <<
257
+ ')'
258
+ end
259
+
260
+ def compile_wildcard(cur_node, name, seq_head)
261
+ if name.empty?
262
+ 'true'
263
+ elsif @unify.key?(name)
264
+ # we have already seen a wildcard with this name before
265
+ # so the value it matched the first time will already be stored
266
+ # in a temp. check if this value matches the one stored in the temp
267
+ "(#{cur_node}#{'.type' if seq_head} == temp#{@unify[name]})"
268
+ else
269
+ n = @unify[name] = (@temps += 1)
270
+ "(temp#{n} = #{cur_node}#{'.type' if seq_head}; true)"
271
+ end
272
+ end
273
+
274
+ def compile_literal(cur_node, literal, seq_head)
275
+ "(#{cur_node}#{'.type' if seq_head} == #{literal})"
276
+ end
277
+
278
+ def compile_predicate(cur_node, predicate, seq_head)
279
+ "(#{cur_node}#{'.type' if seq_head}.#{predicate})"
280
+ end
281
+
282
+ def compile_nodetype(cur_node, type)
283
+ "(#{cur_node} && #{cur_node}.#{type}_type?)"
284
+ end
285
+
286
+ def compile_param(cur_node, number, seq_head)
287
+ number = number.empty? ? 1 : Integer(number)
288
+ @params = number if number > @params
289
+ "(#{cur_node}#{'.type' if seq_head} == param#{number})"
290
+ end
291
+
292
+ def next_capture
293
+ "capture#{@captures += 1}"
294
+ end
295
+
296
+ def join_terms(init, terms, operator)
297
+ '(' << init << ';' << terms.join(operator) << ')'
298
+ end
299
+
300
+ def emit_capture_list
301
+ (1..@captures).map { |n| "capture#{n}" }.join(',')
302
+ end
303
+
304
+ def emit_retval
305
+ if @captures.zero?
306
+ 'true'
307
+ elsif @captures == 1
308
+ 'capture1'
309
+ else
310
+ "[#{emit_capture_list}]"
311
+ end
312
+ end
313
+
314
+ def emit_param_list
315
+ (1..@params).map { |n| "param#{n}" }.join(',')
316
+ end
317
+
318
+ def emit_trailing_param_list
319
+ params = emit_param_list
320
+ params.empty? ? '' : ',' << params
321
+ end
322
+
323
+ def emit_method_code
324
+ <<-CODE
325
+ return nil unless #{@match_code}
326
+ block_given? ? yield(#{emit_capture_list}) : (return #{emit_retval})
327
+ CODE
328
+ end
329
+
330
+ def fail_due_to(message)
331
+ fail Invalid, "Couldn't compile due to #{message}. Pattern: #{@string}"
332
+ end
333
+ end
334
+
335
+ # Helpers for defining methods based on a pattern string
336
+ module Macros
337
+ # Define a method which applies a pattern to an AST node
338
+ #
339
+ # The new method will return nil if the node does not match
340
+ # If the node matches, and a block is provided, the new method will
341
+ # yield to the block (passing any captures as block arguments).
342
+ # If the node matches, and no block is provided, the new method will
343
+ # return the captures, or `true` if there were none.
344
+ def def_node_matcher(method_name, pattern_str)
345
+ compiler = RuboCop::NodePattern::Compiler.new(pattern_str, 'node')
346
+ src = "def #{method_name}(node" << compiler.emit_trailing_param_list <<
347
+ ');' << compiler.emit_method_code << ';end'
348
+ class_eval(src)
349
+ end
350
+
351
+ # Define a method which recurses over the descendants of an AST node,
352
+ # checking whether any of them match the provided pattern
353
+ #
354
+ # If the method name ends with '?', the new method will return `true`
355
+ # as soon as it finds a descendant which matches. Otherwise, it will
356
+ # yield all descendants which match.
357
+ def def_node_search(method_name, pattern_str)
358
+ compiler = RuboCop::NodePattern::Compiler.new(pattern_str, 'node')
359
+ if method_name.to_s.end_with?('?')
360
+ on_match = 'return true'
361
+ prelude = ''
362
+ else
363
+ yieldval = compiler.emit_capture_list
364
+ yieldval = 'node' if yieldval.empty?
365
+ on_match = "yield(#{yieldval})"
366
+ prelude = "return enum_for(:#{method_name},node0) unless block_given?"
367
+ end
368
+ src = <<-END
369
+ def #{method_name}(node0#{compiler.emit_trailing_param_list})
370
+ #{prelude}
371
+ node0.each_node do |node|
372
+ if #{compiler.match_code}
373
+ #{on_match}
374
+ end
375
+ end
376
+ nil
377
+ end
378
+ END
379
+ class_eval(src)
380
+ end
381
+ end
382
+
383
+ def initialize(str)
384
+ compiler = Compiler.new(str)
385
+ src = 'def match(node0' << compiler.emit_trailing_param_list << ');' <<
386
+ compiler.emit_method_code << 'end'
387
+ instance_eval(src)
388
+ end
389
+ end
390
+ end