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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +315 -0
- data/README.md +199 -38
- data/config/default.yml +91 -12
- data/config/disabled.yml +45 -4
- data/config/enabled.yml +107 -9
- data/lib/rubocop/ast_node.rb +48 -0
- data/lib/rubocop/cli.rb +11 -1
- data/lib/rubocop/comment_config.rb +4 -1
- data/lib/rubocop/config.rb +26 -17
- data/lib/rubocop/config_loader.rb +61 -14
- data/lib/rubocop/cop/commissioner.rb +7 -12
- data/lib/rubocop/cop/cop.rb +43 -20
- data/lib/rubocop/cop/lint/block_alignment.rb +1 -1
- data/lib/rubocop/cop/lint/circular_argument_reference.rb +69 -0
- data/lib/rubocop/cop/lint/debugger.rb +9 -48
- data/lib/rubocop/cop/lint/def_end_alignment.rb +8 -4
- data/lib/rubocop/cop/lint/deprecated_class_methods.rb +42 -23
- data/lib/rubocop/cop/lint/duplicate_methods.rb +2 -2
- data/lib/rubocop/cop/lint/duplicated_key.rb +37 -0
- data/lib/rubocop/cop/lint/end_alignment.rb +33 -13
- data/lib/rubocop/cop/lint/eval.rb +6 -2
- data/lib/rubocop/cop/lint/format_parameter_mismatch.rb +175 -0
- data/lib/rubocop/cop/lint/literal_in_condition.rb +0 -5
- data/lib/rubocop/cop/lint/literal_in_interpolation.rb +10 -0
- data/lib/rubocop/cop/lint/nested_method_definition.rb +31 -0
- data/lib/rubocop/cop/lint/non_local_exit_from_iterator.rb +19 -1
- data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +1 -1
- data/lib/rubocop/cop/lint/space_before_first_arg.rb +1 -1
- data/lib/rubocop/cop/lint/unneeded_disable.rb +72 -0
- data/lib/rubocop/cop/lint/unused_block_argument.rb +6 -0
- data/lib/rubocop/cop/lint/unused_method_argument.rb +8 -0
- data/lib/rubocop/cop/metrics/abc_size.rb +17 -6
- data/lib/rubocop/cop/metrics/class_length.rb +1 -1
- data/lib/rubocop/cop/metrics/method_length.rb +1 -3
- data/lib/rubocop/cop/metrics/module_length.rb +1 -1
- data/lib/rubocop/cop/metrics/parameter_lists.rb +1 -1
- data/lib/rubocop/cop/mixin/access_modifier_node.rb +1 -1
- data/lib/rubocop/cop/mixin/annotation_comment.rb +1 -2
- data/lib/rubocop/cop/mixin/autocorrect_alignment.rb +28 -4
- data/lib/rubocop/cop/mixin/autocorrect_unless_changing_ast.rb +26 -3
- data/lib/rubocop/cop/mixin/check_assignment.rb +2 -3
- data/lib/rubocop/cop/mixin/configurable_enforced_style.rb +59 -12
- data/lib/rubocop/cop/mixin/configurable_max.rb +1 -1
- data/lib/rubocop/cop/mixin/configurable_naming.rb +14 -3
- data/lib/rubocop/cop/mixin/empty_lines_around_body.rb +1 -3
- data/lib/rubocop/cop/mixin/end_keyword_alignment.rb +10 -1
- data/lib/rubocop/cop/mixin/first_element_line_break.rb +41 -0
- data/lib/rubocop/cop/mixin/if_node.rb +10 -0
- data/lib/rubocop/cop/mixin/method_preference.rb +28 -0
- data/lib/rubocop/cop/mixin/negative_conditional.rb +1 -1
- data/lib/rubocop/cop/mixin/on_method_def.rb +4 -5
- data/lib/rubocop/cop/mixin/safe_assignment.rb +3 -14
- data/lib/rubocop/cop/mixin/space_after_punctuation.rb +8 -1
- data/lib/rubocop/cop/mixin/space_before_punctuation.rb +8 -1
- data/lib/rubocop/cop/mixin/statement_modifier.rb +4 -7
- data/lib/rubocop/cop/mixin/string_help.rb +1 -1
- data/lib/rubocop/cop/mixin/string_literals_help.rb +1 -1
- data/lib/rubocop/cop/mixin/surrounding_space.rb +5 -4
- data/lib/rubocop/cop/offense.rb +16 -3
- data/lib/rubocop/cop/performance/case_when_splat.rb +160 -0
- data/lib/rubocop/cop/performance/count.rb +35 -30
- data/lib/rubocop/cop/performance/detect.rb +16 -3
- data/lib/rubocop/cop/performance/fixed_size.rb +50 -0
- data/lib/rubocop/cop/performance/flat_map.rb +3 -3
- data/lib/rubocop/cop/performance/sample.rb +103 -59
- data/lib/rubocop/cop/performance/size.rb +2 -1
- data/lib/rubocop/cop/performance/string_replacement.rb +187 -0
- data/lib/rubocop/cop/rails/action_filter.rb +31 -5
- data/lib/rubocop/cop/rails/date.rb +15 -14
- data/lib/rubocop/cop/rails/pluralization_grammar.rb +97 -0
- data/lib/rubocop/cop/rails/read_write_attribute.rb +1 -1
- data/lib/rubocop/cop/rails/time_zone.rb +46 -18
- data/lib/rubocop/cop/style/alias.rb +1 -0
- data/lib/rubocop/cop/style/align_hash.rb +8 -15
- data/lib/rubocop/cop/style/align_parameters.rb +19 -7
- data/lib/rubocop/cop/style/and_or.rb +42 -13
- data/lib/rubocop/cop/style/auto_resource_cleanup.rb +2 -1
- data/lib/rubocop/cop/style/block_comments.rb +4 -2
- data/lib/rubocop/cop/style/block_delimiters.rb +69 -24
- data/lib/rubocop/cop/style/braces_around_hash_parameters.rb +40 -12
- data/lib/rubocop/cop/style/case_indentation.rb +18 -4
- data/lib/rubocop/cop/style/collection_methods.rb +2 -20
- data/lib/rubocop/cop/style/command_literal.rb +2 -10
- data/lib/rubocop/cop/style/comment_annotation.rb +29 -8
- data/lib/rubocop/cop/style/copyright.rb +5 -3
- data/lib/rubocop/cop/style/documentation.rb +21 -12
- data/lib/rubocop/cop/style/dot_position.rb +6 -0
- data/lib/rubocop/cop/style/double_negation.rb +4 -15
- data/lib/rubocop/cop/style/each_with_object.rb +17 -4
- data/lib/rubocop/cop/style/else_alignment.rb +2 -1
- data/lib/rubocop/cop/style/empty_else.rb +25 -0
- data/lib/rubocop/cop/style/empty_line_between_defs.rb +39 -14
- data/lib/rubocop/cop/style/encoding.rb +10 -4
- data/lib/rubocop/cop/style/extra_spacing.rb +126 -5
- data/lib/rubocop/cop/style/first_array_element_line_break.rb +41 -0
- data/lib/rubocop/cop/style/first_hash_element_line_break.rb +35 -0
- data/lib/rubocop/cop/style/first_method_argument_line_break.rb +37 -0
- data/lib/rubocop/cop/style/first_method_parameter_line_break.rb +42 -0
- data/lib/rubocop/cop/style/first_parameter_indentation.rb +5 -3
- data/lib/rubocop/cop/style/for.rb +2 -1
- data/lib/rubocop/cop/style/hash_syntax.rb +5 -0
- data/lib/rubocop/cop/style/if_unless_modifier.rb +32 -5
- data/lib/rubocop/cop/style/indent_hash.rb +67 -37
- data/lib/rubocop/cop/style/indentation_width.rb +36 -10
- data/lib/rubocop/cop/style/initial_indentation.rb +37 -0
- data/lib/rubocop/cop/style/leading_comment_space.rb +3 -2
- data/lib/rubocop/cop/style/method_call_parentheses.rb +28 -1
- data/lib/rubocop/cop/style/method_def_parentheses.rb +10 -7
- data/lib/rubocop/cop/style/multiline_operation_indentation.rb +21 -24
- data/lib/rubocop/cop/style/mutable_constant.rb +35 -0
- data/lib/rubocop/cop/style/nested_modifier.rb +97 -0
- data/lib/rubocop/cop/style/next.rb +50 -15
- data/lib/rubocop/cop/style/non_nil_check.rb +12 -8
- data/lib/rubocop/cop/style/one_line_conditional.rb +8 -4
- data/lib/rubocop/cop/style/option_hash.rb +64 -0
- data/lib/rubocop/cop/style/optional_arguments.rb +49 -0
- data/lib/rubocop/cop/style/parallel_assignment.rb +218 -0
- data/lib/rubocop/cop/style/percent_literal_delimiters.rb +3 -66
- data/lib/rubocop/cop/style/predicate_name.rb +7 -2
- data/lib/rubocop/cop/style/redundant_begin.rb +2 -13
- data/lib/rubocop/cop/style/redundant_freeze.rb +37 -0
- data/lib/rubocop/cop/style/redundant_return.rb +32 -3
- data/lib/rubocop/cop/style/regexp_literal.rb +2 -10
- data/lib/rubocop/cop/style/rescue_ensure_alignment.rb +81 -0
- data/lib/rubocop/cop/style/rescue_modifier.rb +30 -22
- data/lib/rubocop/cop/style/send.rb +18 -0
- data/lib/rubocop/cop/style/signal_exception.rb +24 -11
- data/lib/rubocop/cop/style/single_line_methods.rb +8 -9
- data/lib/rubocop/cop/style/single_space_before_first_arg.rb +1 -1
- data/lib/rubocop/cop/style/space_around_operators.rb +2 -0
- data/lib/rubocop/cop/style/space_inside_string_interpolation.rb +61 -0
- data/lib/rubocop/cop/style/special_global_vars.rb +4 -2
- data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +108 -0
- data/lib/rubocop/cop/style/string_methods.rb +32 -0
- data/lib/rubocop/cop/style/struct_inheritance.rb +11 -10
- data/lib/rubocop/cop/style/symbol_literal.rb +1 -1
- data/lib/rubocop/cop/style/symbol_proc.rb +62 -13
- data/lib/rubocop/cop/style/trailing_blank_lines.rb +9 -1
- data/lib/rubocop/cop/style/trailing_comma.rb +17 -7
- data/lib/rubocop/cop/style/trailing_underscore_variable.rb +23 -2
- data/lib/rubocop/cop/style/trivial_accessors.rb +10 -1
- data/lib/rubocop/cop/style/unneeded_percent_q.rb +31 -20
- data/lib/rubocop/cop/style/variable_name.rb +5 -0
- data/lib/rubocop/cop/style/while_until_do.rb +1 -1
- data/lib/rubocop/cop/style/word_array.rb +15 -2
- data/lib/rubocop/cop/team.rb +25 -5
- data/lib/rubocop/cop/util.rb +7 -2
- data/lib/rubocop/cop/variable_force/locatable.rb +6 -6
- data/lib/rubocop/cop/variable_force.rb +10 -10
- data/lib/rubocop/formatter/base_formatter.rb +1 -1
- data/lib/rubocop/formatter/disabled_config_formatter.rb +70 -8
- data/lib/rubocop/formatter/formatter_set.rb +27 -1
- data/lib/rubocop/formatter/progress_formatter.rb +10 -2
- data/lib/rubocop/formatter/simple_text_formatter.rb +1 -1
- data/lib/rubocop/node_pattern.rb +390 -0
- data/lib/rubocop/options.rb +148 -81
- data/lib/rubocop/processed_source.rb +7 -2
- data/lib/rubocop/rake_task.rb +1 -1
- data/lib/rubocop/remote_config.rb +60 -0
- data/lib/rubocop/result_cache.rb +123 -0
- data/lib/rubocop/runner.rb +85 -22
- data/lib/rubocop/target_finder.rb +4 -4
- data/lib/rubocop/token.rb +2 -1
- data/lib/rubocop/version.rb +1 -1
- data/lib/rubocop/warning.rb +11 -0
- data/lib/rubocop.rb +32 -3
- data/relnotes/v0.32.0.md +139 -0
- data/relnotes/v0.32.1.md +122 -0
- data/relnotes/v0.33.0.md +157 -0
- data/relnotes/v0.34.0.md +182 -0
- data/relnotes/v0.34.1.md +129 -0
- data/relnotes/v0.34.2.md +139 -0
- data/relnotes/v0.35.0.md +210 -0
- data/rubocop.gemspec +4 -4
- metadata +50 -12
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 ||= {
|
|
57
|
+
cfg ||= {}
|
|
40
58
|
output_cop_comments(output, cfg, cop_name, offense_count)
|
|
41
|
-
output
|
|
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 -
|
|
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, :
|
|
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.
|
|
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
|
-
|
|
54
|
+
@dot
|
|
47
55
|
else
|
|
48
56
|
highest_offense = offenses.max_by(&:severity)
|
|
49
57
|
colored_severity_code(highest_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
|