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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +86 -0
- data/README.md +103 -31
- data/config/default.yml +32 -2
- data/config/disabled.yml +24 -0
- data/config/enabled.yml +20 -2
- data/lib/rubocop.rb +13 -0
- data/lib/rubocop/ast_node.rb +48 -0
- data/lib/rubocop/cli.rb +9 -0
- data/lib/rubocop/config.rb +8 -6
- data/lib/rubocop/config_loader.rb +30 -8
- data/lib/rubocop/cop/commissioner.rb +1 -1
- data/lib/rubocop/cop/cop.rb +19 -6
- data/lib/rubocop/cop/lint/circular_argument_reference.rb +33 -2
- data/lib/rubocop/cop/lint/debugger.rb +9 -56
- data/lib/rubocop/cop/lint/end_alignment.rb +29 -9
- data/lib/rubocop/cop/lint/eval.rb +6 -2
- data/lib/rubocop/cop/lint/format_parameter_mismatch.rb +24 -6
- data/lib/rubocop/cop/lint/literal_in_condition.rb +0 -5
- data/lib/rubocop/cop/lint/non_local_exit_from_iterator.rb +10 -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/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 +1 -1
- data/lib/rubocop/cop/mixin/access_modifier_node.rb +1 -1
- data/lib/rubocop/cop/mixin/autocorrect_alignment.rb +1 -1
- 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 +1 -1
- data/lib/rubocop/cop/mixin/first_element_line_break.rb +41 -0
- data/lib/rubocop/cop/mixin/negative_conditional.rb +1 -1
- data/lib/rubocop/cop/mixin/safe_assignment.rb +3 -14
- data/lib/rubocop/cop/mixin/statement_modifier.rb +2 -2
- data/lib/rubocop/cop/performance/detect.rb +5 -1
- data/lib/rubocop/cop/performance/fixed_size.rb +50 -0
- data/lib/rubocop/cop/performance/size.rb +1 -1
- data/lib/rubocop/cop/performance/string_replacement.rb +14 -8
- data/lib/rubocop/cop/rails/pluralization_grammar.rb +97 -0
- data/lib/rubocop/cop/style/align_hash.rb +1 -12
- 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/block_comments.rb +4 -2
- data/lib/rubocop/cop/style/block_delimiters.rb +57 -18
- data/lib/rubocop/cop/style/braces_around_hash_parameters.rb +1 -1
- data/lib/rubocop/cop/style/command_literal.rb +2 -10
- data/lib/rubocop/cop/style/copyright.rb +5 -3
- data/lib/rubocop/cop/style/documentation.rb +9 -6
- 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/empty_line_between_defs.rb +1 -5
- data/lib/rubocop/cop/style/encoding.rb +10 -4
- data/lib/rubocop/cop/style/extra_spacing.rb +23 -13
- 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/for.rb +2 -1
- data/lib/rubocop/cop/style/if_unless_modifier.rb +31 -0
- data/lib/rubocop/cop/style/indent_hash.rb +67 -37
- data/lib/rubocop/cop/style/indentation_width.rb +1 -1
- data/lib/rubocop/cop/style/leading_comment_space.rb +3 -2
- data/lib/rubocop/cop/style/method_call_parentheses.rb +8 -0
- data/lib/rubocop/cop/style/method_def_parentheses.rb +10 -7
- data/lib/rubocop/cop/style/multiline_operation_indentation.rb +8 -13
- data/lib/rubocop/cop/style/nested_modifier.rb +97 -0
- data/lib/rubocop/cop/style/next.rb +18 -0
- data/lib/rubocop/cop/style/parallel_assignment.rb +57 -15
- data/lib/rubocop/cop/style/predicate_name.rb +7 -2
- data/lib/rubocop/cop/style/regexp_literal.rb +2 -10
- data/lib/rubocop/cop/style/single_line_methods.rb +7 -5
- 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/special_global_vars.rb +4 -2
- data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +108 -0
- data/lib/rubocop/cop/style/trailing_comma.rb +9 -6
- data/lib/rubocop/cop/style/trailing_underscore_variable.rb +23 -2
- 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/word_array.rb +2 -1
- data/lib/rubocop/cop/team.rb +17 -4
- data/lib/rubocop/cop/util.rb +5 -0
- data/lib/rubocop/cop/variable_force/locatable.rb +1 -1
- data/lib/rubocop/formatter/base_formatter.rb +1 -1
- data/lib/rubocop/formatter/disabled_config_formatter.rb +22 -10
- data/lib/rubocop/formatter/simple_text_formatter.rb +1 -1
- data/lib/rubocop/node_pattern.rb +390 -0
- data/lib/rubocop/options.rb +48 -36
- data/lib/rubocop/processed_source.rb +3 -1
- data/lib/rubocop/rake_task.rb +1 -1
- data/lib/rubocop/remote_config.rb +60 -0
- data/lib/rubocop/result_cache.rb +4 -2
- data/lib/rubocop/runner.rb +33 -10
- data/lib/rubocop/token.rb +2 -1
- data/lib/rubocop/version.rb +1 -1
- data/lib/rubocop/warning.rb +11 -0
- data/relnotes/v0.35.0.md +210 -0
- data/rubocop.gemspec +2 -2
- 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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
37
|
-
return if 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?(
|
41
|
-
|
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 =
|
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
|
@@ -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 +
|
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
|
data/lib/rubocop/cop/team.rb
CHANGED
@@ -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
|
-
|
100
|
-
|
101
|
-
|
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
|
data/lib/rubocop/cop/util.rb
CHANGED
@@ -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(
|
@@ -33,11 +33,11 @@ module RuboCop
|
|
33
33
|
|
34
34
|
def file_finished(file, offenses)
|
35
35
|
@cops_with_offenses ||= Hash.new(0)
|
36
|
-
@
|
36
|
+
@files_with_offenses ||= {}
|
37
37
|
offenses.each do |o|
|
38
38
|
@cops_with_offenses[o.cop_name] += 1
|
39
|
-
@
|
40
|
-
@
|
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
|
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 = @
|
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)
|
@@ -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
|