rubocop 0.31.0 → 0.35.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 (177) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +315 -0
  3. data/README.md +199 -38
  4. data/config/default.yml +91 -12
  5. data/config/disabled.yml +45 -4
  6. data/config/enabled.yml +107 -9
  7. data/lib/rubocop/ast_node.rb +48 -0
  8. data/lib/rubocop/cli.rb +11 -1
  9. data/lib/rubocop/comment_config.rb +4 -1
  10. data/lib/rubocop/config.rb +26 -17
  11. data/lib/rubocop/config_loader.rb +61 -14
  12. data/lib/rubocop/cop/commissioner.rb +7 -12
  13. data/lib/rubocop/cop/cop.rb +43 -20
  14. data/lib/rubocop/cop/lint/block_alignment.rb +1 -1
  15. data/lib/rubocop/cop/lint/circular_argument_reference.rb +69 -0
  16. data/lib/rubocop/cop/lint/debugger.rb +9 -48
  17. data/lib/rubocop/cop/lint/def_end_alignment.rb +8 -4
  18. data/lib/rubocop/cop/lint/deprecated_class_methods.rb +42 -23
  19. data/lib/rubocop/cop/lint/duplicate_methods.rb +2 -2
  20. data/lib/rubocop/cop/lint/duplicated_key.rb +37 -0
  21. data/lib/rubocop/cop/lint/end_alignment.rb +33 -13
  22. data/lib/rubocop/cop/lint/eval.rb +6 -2
  23. data/lib/rubocop/cop/lint/format_parameter_mismatch.rb +175 -0
  24. data/lib/rubocop/cop/lint/literal_in_condition.rb +0 -5
  25. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +10 -0
  26. data/lib/rubocop/cop/lint/nested_method_definition.rb +31 -0
  27. data/lib/rubocop/cop/lint/non_local_exit_from_iterator.rb +19 -1
  28. data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +1 -1
  29. data/lib/rubocop/cop/lint/space_before_first_arg.rb +1 -1
  30. data/lib/rubocop/cop/lint/unneeded_disable.rb +72 -0
  31. data/lib/rubocop/cop/lint/unused_block_argument.rb +6 -0
  32. data/lib/rubocop/cop/lint/unused_method_argument.rb +8 -0
  33. data/lib/rubocop/cop/metrics/abc_size.rb +17 -6
  34. data/lib/rubocop/cop/metrics/class_length.rb +1 -1
  35. data/lib/rubocop/cop/metrics/method_length.rb +1 -3
  36. data/lib/rubocop/cop/metrics/module_length.rb +1 -1
  37. data/lib/rubocop/cop/metrics/parameter_lists.rb +1 -1
  38. data/lib/rubocop/cop/mixin/access_modifier_node.rb +1 -1
  39. data/lib/rubocop/cop/mixin/annotation_comment.rb +1 -2
  40. data/lib/rubocop/cop/mixin/autocorrect_alignment.rb +28 -4
  41. data/lib/rubocop/cop/mixin/autocorrect_unless_changing_ast.rb +26 -3
  42. data/lib/rubocop/cop/mixin/check_assignment.rb +2 -3
  43. data/lib/rubocop/cop/mixin/configurable_enforced_style.rb +59 -12
  44. data/lib/rubocop/cop/mixin/configurable_max.rb +1 -1
  45. data/lib/rubocop/cop/mixin/configurable_naming.rb +14 -3
  46. data/lib/rubocop/cop/mixin/empty_lines_around_body.rb +1 -3
  47. data/lib/rubocop/cop/mixin/end_keyword_alignment.rb +10 -1
  48. data/lib/rubocop/cop/mixin/first_element_line_break.rb +41 -0
  49. data/lib/rubocop/cop/mixin/if_node.rb +10 -0
  50. data/lib/rubocop/cop/mixin/method_preference.rb +28 -0
  51. data/lib/rubocop/cop/mixin/negative_conditional.rb +1 -1
  52. data/lib/rubocop/cop/mixin/on_method_def.rb +4 -5
  53. data/lib/rubocop/cop/mixin/safe_assignment.rb +3 -14
  54. data/lib/rubocop/cop/mixin/space_after_punctuation.rb +8 -1
  55. data/lib/rubocop/cop/mixin/space_before_punctuation.rb +8 -1
  56. data/lib/rubocop/cop/mixin/statement_modifier.rb +4 -7
  57. data/lib/rubocop/cop/mixin/string_help.rb +1 -1
  58. data/lib/rubocop/cop/mixin/string_literals_help.rb +1 -1
  59. data/lib/rubocop/cop/mixin/surrounding_space.rb +5 -4
  60. data/lib/rubocop/cop/offense.rb +16 -3
  61. data/lib/rubocop/cop/performance/case_when_splat.rb +160 -0
  62. data/lib/rubocop/cop/performance/count.rb +35 -30
  63. data/lib/rubocop/cop/performance/detect.rb +16 -3
  64. data/lib/rubocop/cop/performance/fixed_size.rb +50 -0
  65. data/lib/rubocop/cop/performance/flat_map.rb +3 -3
  66. data/lib/rubocop/cop/performance/sample.rb +103 -59
  67. data/lib/rubocop/cop/performance/size.rb +2 -1
  68. data/lib/rubocop/cop/performance/string_replacement.rb +187 -0
  69. data/lib/rubocop/cop/rails/action_filter.rb +31 -5
  70. data/lib/rubocop/cop/rails/date.rb +15 -14
  71. data/lib/rubocop/cop/rails/pluralization_grammar.rb +97 -0
  72. data/lib/rubocop/cop/rails/read_write_attribute.rb +1 -1
  73. data/lib/rubocop/cop/rails/time_zone.rb +46 -18
  74. data/lib/rubocop/cop/style/alias.rb +1 -0
  75. data/lib/rubocop/cop/style/align_hash.rb +8 -15
  76. data/lib/rubocop/cop/style/align_parameters.rb +19 -7
  77. data/lib/rubocop/cop/style/and_or.rb +42 -13
  78. data/lib/rubocop/cop/style/auto_resource_cleanup.rb +2 -1
  79. data/lib/rubocop/cop/style/block_comments.rb +4 -2
  80. data/lib/rubocop/cop/style/block_delimiters.rb +69 -24
  81. data/lib/rubocop/cop/style/braces_around_hash_parameters.rb +40 -12
  82. data/lib/rubocop/cop/style/case_indentation.rb +18 -4
  83. data/lib/rubocop/cop/style/collection_methods.rb +2 -20
  84. data/lib/rubocop/cop/style/command_literal.rb +2 -10
  85. data/lib/rubocop/cop/style/comment_annotation.rb +29 -8
  86. data/lib/rubocop/cop/style/copyright.rb +5 -3
  87. data/lib/rubocop/cop/style/documentation.rb +21 -12
  88. data/lib/rubocop/cop/style/dot_position.rb +6 -0
  89. data/lib/rubocop/cop/style/double_negation.rb +4 -15
  90. data/lib/rubocop/cop/style/each_with_object.rb +17 -4
  91. data/lib/rubocop/cop/style/else_alignment.rb +2 -1
  92. data/lib/rubocop/cop/style/empty_else.rb +25 -0
  93. data/lib/rubocop/cop/style/empty_line_between_defs.rb +39 -14
  94. data/lib/rubocop/cop/style/encoding.rb +10 -4
  95. data/lib/rubocop/cop/style/extra_spacing.rb +126 -5
  96. data/lib/rubocop/cop/style/first_array_element_line_break.rb +41 -0
  97. data/lib/rubocop/cop/style/first_hash_element_line_break.rb +35 -0
  98. data/lib/rubocop/cop/style/first_method_argument_line_break.rb +37 -0
  99. data/lib/rubocop/cop/style/first_method_parameter_line_break.rb +42 -0
  100. data/lib/rubocop/cop/style/first_parameter_indentation.rb +5 -3
  101. data/lib/rubocop/cop/style/for.rb +2 -1
  102. data/lib/rubocop/cop/style/hash_syntax.rb +5 -0
  103. data/lib/rubocop/cop/style/if_unless_modifier.rb +32 -5
  104. data/lib/rubocop/cop/style/indent_hash.rb +67 -37
  105. data/lib/rubocop/cop/style/indentation_width.rb +36 -10
  106. data/lib/rubocop/cop/style/initial_indentation.rb +37 -0
  107. data/lib/rubocop/cop/style/leading_comment_space.rb +3 -2
  108. data/lib/rubocop/cop/style/method_call_parentheses.rb +28 -1
  109. data/lib/rubocop/cop/style/method_def_parentheses.rb +10 -7
  110. data/lib/rubocop/cop/style/multiline_operation_indentation.rb +21 -24
  111. data/lib/rubocop/cop/style/mutable_constant.rb +35 -0
  112. data/lib/rubocop/cop/style/nested_modifier.rb +97 -0
  113. data/lib/rubocop/cop/style/next.rb +50 -15
  114. data/lib/rubocop/cop/style/non_nil_check.rb +12 -8
  115. data/lib/rubocop/cop/style/one_line_conditional.rb +8 -4
  116. data/lib/rubocop/cop/style/option_hash.rb +64 -0
  117. data/lib/rubocop/cop/style/optional_arguments.rb +49 -0
  118. data/lib/rubocop/cop/style/parallel_assignment.rb +218 -0
  119. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +3 -66
  120. data/lib/rubocop/cop/style/predicate_name.rb +7 -2
  121. data/lib/rubocop/cop/style/redundant_begin.rb +2 -13
  122. data/lib/rubocop/cop/style/redundant_freeze.rb +37 -0
  123. data/lib/rubocop/cop/style/redundant_return.rb +32 -3
  124. data/lib/rubocop/cop/style/regexp_literal.rb +2 -10
  125. data/lib/rubocop/cop/style/rescue_ensure_alignment.rb +81 -0
  126. data/lib/rubocop/cop/style/rescue_modifier.rb +30 -22
  127. data/lib/rubocop/cop/style/send.rb +18 -0
  128. data/lib/rubocop/cop/style/signal_exception.rb +24 -11
  129. data/lib/rubocop/cop/style/single_line_methods.rb +8 -9
  130. data/lib/rubocop/cop/style/single_space_before_first_arg.rb +1 -1
  131. data/lib/rubocop/cop/style/space_around_operators.rb +2 -0
  132. data/lib/rubocop/cop/style/space_inside_string_interpolation.rb +61 -0
  133. data/lib/rubocop/cop/style/special_global_vars.rb +4 -2
  134. data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +108 -0
  135. data/lib/rubocop/cop/style/string_methods.rb +32 -0
  136. data/lib/rubocop/cop/style/struct_inheritance.rb +11 -10
  137. data/lib/rubocop/cop/style/symbol_literal.rb +1 -1
  138. data/lib/rubocop/cop/style/symbol_proc.rb +62 -13
  139. data/lib/rubocop/cop/style/trailing_blank_lines.rb +9 -1
  140. data/lib/rubocop/cop/style/trailing_comma.rb +17 -7
  141. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +23 -2
  142. data/lib/rubocop/cop/style/trivial_accessors.rb +10 -1
  143. data/lib/rubocop/cop/style/unneeded_percent_q.rb +31 -20
  144. data/lib/rubocop/cop/style/variable_name.rb +5 -0
  145. data/lib/rubocop/cop/style/while_until_do.rb +1 -1
  146. data/lib/rubocop/cop/style/word_array.rb +15 -2
  147. data/lib/rubocop/cop/team.rb +25 -5
  148. data/lib/rubocop/cop/util.rb +7 -2
  149. data/lib/rubocop/cop/variable_force/locatable.rb +6 -6
  150. data/lib/rubocop/cop/variable_force.rb +10 -10
  151. data/lib/rubocop/formatter/base_formatter.rb +1 -1
  152. data/lib/rubocop/formatter/disabled_config_formatter.rb +70 -8
  153. data/lib/rubocop/formatter/formatter_set.rb +27 -1
  154. data/lib/rubocop/formatter/progress_formatter.rb +10 -2
  155. data/lib/rubocop/formatter/simple_text_formatter.rb +1 -1
  156. data/lib/rubocop/node_pattern.rb +390 -0
  157. data/lib/rubocop/options.rb +148 -81
  158. data/lib/rubocop/processed_source.rb +7 -2
  159. data/lib/rubocop/rake_task.rb +1 -1
  160. data/lib/rubocop/remote_config.rb +60 -0
  161. data/lib/rubocop/result_cache.rb +123 -0
  162. data/lib/rubocop/runner.rb +85 -22
  163. data/lib/rubocop/target_finder.rb +4 -4
  164. data/lib/rubocop/token.rb +2 -1
  165. data/lib/rubocop/version.rb +1 -1
  166. data/lib/rubocop/warning.rb +11 -0
  167. data/lib/rubocop.rb +32 -3
  168. data/relnotes/v0.32.0.md +139 -0
  169. data/relnotes/v0.32.1.md +122 -0
  170. data/relnotes/v0.33.0.md +157 -0
  171. data/relnotes/v0.34.0.md +182 -0
  172. data/relnotes/v0.34.1.md +129 -0
  173. data/relnotes/v0.34.2.md +139 -0
  174. data/relnotes/v0.35.0.md +210 -0
  175. data/rubocop.gemspec +4 -4
  176. metadata +50 -12
  177. data/lib/rubocop/cop/performance/parallel_assignment.rb +0 -79
@@ -6,7 +6,8 @@ module RuboCop
6
6
  # detected any offenses are configured to not detect the offense.
7
7
  class DisabledConfigFormatter < BaseFormatter
8
8
  HEADING =
9
- ['# This configuration was generated by `rubocop --auto-gen-config`',
9
+ ['# This configuration was generated by',
10
+ '# `%s`',
10
11
  "# on #{Time.now} using RuboCop version #{Version.version}.",
11
12
  '# The point is for the user to remove these configuration records',
12
13
  '# one by one as the offenses are removed from the code base.',
@@ -22,13 +23,30 @@ module RuboCop
22
23
  attr_accessor :config_to_allow_offenses
23
24
  end
24
25
 
25
- def file_finished(_file, offenses)
26
+ def file_started(_file, file_info)
27
+ @exclude_limit_option =
28
+ file_info.fetch(:cli_options, {})[:exclude_limit]
29
+ @exclude_limit = (
30
+ @exclude_limit_option ||
31
+ RuboCop::Options::DEFAULT_MAXIMUM_EXCLUSION_ITEMS).to_i
32
+ end
33
+
34
+ def file_finished(file, offenses)
26
35
  @cops_with_offenses ||= Hash.new(0)
27
- offenses.each { |o| @cops_with_offenses[o.cop_name] += 1 }
36
+ @files_with_offenses ||= {}
37
+ offenses.each do |o|
38
+ @cops_with_offenses[o.cop_name] += 1
39
+ @files_with_offenses[o.cop_name] ||= []
40
+ @files_with_offenses[o.cop_name] << file
41
+ end
28
42
  end
29
43
 
30
44
  def finished(_inspected_files)
31
- output.puts HEADING
45
+ command = 'rubocop --auto-gen-config'
46
+ if @exclude_limit_option
47
+ command += format(' --exclude-limit %d', @exclude_limit_option.to_i)
48
+ end
49
+ output.puts HEADING % command
32
50
 
33
51
  # Syntax isn't a real cop and it can't be disabled.
34
52
  @cops_with_offenses.delete('Syntax')
@@ -36,10 +54,9 @@ module RuboCop
36
54
  @cops_with_offenses.sort.each do |cop_name, offense_count|
37
55
  output.puts
38
56
  cfg = self.class.config_to_allow_offenses[cop_name]
39
- cfg ||= { 'Enabled' => false }
57
+ cfg ||= {}
40
58
  output_cop_comments(output, cfg, cop_name, offense_count)
41
- output.puts "#{cop_name}:"
42
- cfg.each { |key, value| output.puts " #{key}: #{value}" }
59
+ output_cop_config(output, cfg, cop_name)
43
60
  end
44
61
  puts "Created #{output.path}."
45
62
  puts "Run `rubocop --config #{output.path}`, or"
@@ -55,12 +72,57 @@ module RuboCop
55
72
  default_cfg = RuboCop::ConfigLoader.default_configuration[cop_name]
56
73
  return unless default_cfg
57
74
 
58
- params = default_cfg.keys - %w(Description StyleGuide Enabled) -
75
+ params = default_cfg.keys -
76
+ %w(Description StyleGuide Reference Enabled) -
59
77
  cfg.keys
60
78
  return if params.empty?
61
79
 
62
80
  output.puts "# Configuration parameters: #{params.join(', ')}."
63
81
  end
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
+
92
+ def output_offending_files(output, cfg, cop_name)
93
+ return unless cfg.empty?
94
+
95
+ offending_files = @files_with_offenses[cop_name].uniq.sort
96
+ if offending_files.count > @exclude_limit
97
+ output.puts ' Enabled: false'
98
+ else
99
+ output_exclude_list(output, offending_files, cop_name)
100
+ end
101
+ end
102
+
103
+ def output_exclude_list(output, offending_files, cop_name)
104
+ require 'pathname'
105
+ parent = Pathname.new(Dir.pwd)
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
+
112
+ output.puts ' Exclude:'
113
+ (default_excludes + offending_files).each do |file|
114
+ file_path = Pathname.new(file)
115
+ begin
116
+ relative = file_path.relative_path_from(parent)
117
+ output.puts " - '#{relative}'"
118
+ rescue ArgumentError
119
+ output.puts " - '#{file}'"
120
+ end
121
+ end
122
+ rescue LoadError
123
+ # Fallback to Enabled: false for Ruby < 1.9.3
124
+ output.puts ' Enabled: false'
125
+ end
64
126
  end
65
127
  end
66
128
  end
@@ -21,7 +21,7 @@ module RuboCop
21
21
  'disabled' => DisabledLinesFormatter
22
22
  }
23
23
 
24
- FORMATTER_APIS = [:started, :file_started, :file_finished, :finished]
24
+ FORMATTER_APIS = [:started, :finished]
25
25
 
26
26
  FORMATTER_APIS.each do |method_name|
27
27
  define_method(method_name) do |*args|
@@ -29,6 +29,32 @@ module RuboCop
29
29
  end
30
30
  end
31
31
 
32
+ def file_started(file, options)
33
+ @cop_disabled_line_ranges = options[:cop_disabled_line_ranges]
34
+ @comments = options[:comments]
35
+ @options = options[:cli_options]
36
+ @config_store = options[:config_store]
37
+ each { |f| f.file_started(file, options) }
38
+ end
39
+
40
+ def file_finished(file, offenses)
41
+ if @cop_disabled_line_ranges.any? &&
42
+ # Don't check unneeded disable if --only or --except option is
43
+ # given, because these options override configuration.
44
+ (@options[:except] || []).empty? && (@options[:only] || []).empty?
45
+ config = @config_store.for(file)
46
+ if config['Lint/UnneededDisable']['Enabled']
47
+ cop = Cop::Lint::UnneededDisable.new(config, @options)
48
+ cop.check(offenses, @cop_disabled_line_ranges, @comments)
49
+ offenses += cop.offenses
50
+ end
51
+ end
52
+
53
+ offenses = offenses.sort.reject(&:disabled?)
54
+ each { |f| f.file_finished(file, offenses) }
55
+ offenses
56
+ end
57
+
32
58
  def add_formatter(formatter_type, output_path = nil)
33
59
  formatter_class = case formatter_type
34
60
  when Class
@@ -8,6 +8,14 @@ module RuboCop
8
8
  class ProgressFormatter < ClangStyleFormatter
9
9
  include TextUtil
10
10
 
11
+ DOT = '.'.freeze
12
+ GREEN_DOT = Rainbow(DOT).green.freeze
13
+
14
+ def initialize(output)
15
+ super
16
+ @dot = @output.tty? ? GREEN_DOT : DOT
17
+ end
18
+
11
19
  def started(target_files)
12
20
  super
13
21
  @offenses_for_files = {}
@@ -36,14 +44,14 @@ module RuboCop
36
44
  end
37
45
  end
38
46
 
39
- report_summary(inspected_files.count,
47
+ report_summary(inspected_files.size,
40
48
  @total_offense_count,
41
49
  @total_correction_count)
42
50
  end
43
51
 
44
52
  def report_file_as_mark(offenses)
45
53
  mark = if offenses.empty?
46
- green('.')
54
+ @dot
47
55
  else
48
56
  highest_offense = offenses.max_by(&:severity)
49
57
  colored_severity_code(highest_offense)
@@ -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