rubocop 1.0.0 → 1.3.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/README.md +36 -16
- data/config/default.yml +141 -14
- data/exe/rubocop +1 -1
- data/lib/rubocop.rb +16 -0
- data/lib/rubocop/cli/command/auto_genenerate_config.rb +1 -1
- data/lib/rubocop/comment_config.rb +1 -1
- data/lib/rubocop/config_loader.rb +7 -6
- data/lib/rubocop/cop/bundler/duplicated_gem.rb +26 -6
- data/lib/rubocop/cop/bundler/gem_comment.rb +1 -1
- data/lib/rubocop/cop/commissioner.rb +10 -10
- data/lib/rubocop/cop/corrector.rb +3 -1
- data/lib/rubocop/cop/force.rb +1 -1
- data/lib/rubocop/cop/gemspec/duplicated_assignment.rb +3 -3
- data/lib/rubocop/cop/gemspec/required_ruby_version.rb +4 -5
- data/lib/rubocop/cop/gemspec/ruby_version_globals_usage.rb +1 -1
- data/lib/rubocop/cop/generator.rb +1 -1
- data/lib/rubocop/cop/layout/block_alignment.rb +3 -4
- data/lib/rubocop/cop/layout/def_end_alignment.rb +1 -1
- data/lib/rubocop/cop/layout/else_alignment.rb +15 -2
- data/lib/rubocop/cop/layout/empty_lines_around_access_modifier.rb +1 -0
- data/lib/rubocop/cop/layout/end_alignment.rb +3 -3
- data/lib/rubocop/cop/layout/extra_spacing.rb +1 -2
- data/lib/rubocop/cop/layout/hash_alignment.rb +4 -4
- data/lib/rubocop/cop/layout/line_length.rb +8 -1
- data/lib/rubocop/cop/layout/space_around_block_parameters.rb +24 -18
- data/lib/rubocop/cop/layout/space_inside_parens.rb +35 -13
- data/lib/rubocop/cop/layout/trailing_whitespace.rb +1 -1
- data/lib/rubocop/cop/lint/constant_definition_in_block.rb +23 -2
- data/lib/rubocop/cop/lint/debugger.rb +17 -28
- data/lib/rubocop/cop/lint/duplicate_branch.rb +93 -0
- data/lib/rubocop/cop/lint/duplicate_case_condition.rb +2 -12
- data/lib/rubocop/cop/lint/duplicate_regexp_character_class_element.rb +77 -0
- data/lib/rubocop/cop/lint/else_layout.rb +29 -3
- data/lib/rubocop/cop/lint/empty_block.rb +82 -0
- data/lib/rubocop/cop/lint/empty_class.rb +93 -0
- data/lib/rubocop/cop/lint/flip_flop.rb +8 -2
- data/lib/rubocop/cop/lint/literal_in_interpolation.rb +38 -6
- data/lib/rubocop/cop/lint/loop.rb +4 -4
- data/lib/rubocop/cop/lint/nested_percent_literal.rb +14 -0
- data/lib/rubocop/cop/lint/no_return_in_begin_end_blocks.rb +58 -0
- data/lib/rubocop/cop/lint/number_conversion.rb +46 -13
- data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +27 -8
- data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +19 -16
- data/lib/rubocop/cop/lint/shadowed_exception.rb +4 -5
- data/lib/rubocop/cop/lint/to_enum_arguments.rb +95 -0
- data/lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb +185 -0
- data/lib/rubocop/cop/lint/useless_access_modifier.rb +2 -2
- data/lib/rubocop/cop/lint/useless_method_definition.rb +2 -4
- data/lib/rubocop/cop/lint/useless_setter_call.rb +6 -1
- data/lib/rubocop/cop/mixin/check_line_breakable.rb +1 -1
- data/lib/rubocop/cop/mixin/configurable_numbering.rb +3 -3
- data/lib/rubocop/cop/mixin/line_length_help.rb +1 -1
- data/lib/rubocop/cop/mixin/statement_modifier.rb +9 -4
- data/lib/rubocop/cop/naming/binary_operator_parameter_name.rb +11 -1
- data/lib/rubocop/cop/naming/heredoc_delimiter_case.rb +11 -5
- data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +67 -18
- data/lib/rubocop/cop/naming/predicate_name.rb +2 -1
- data/lib/rubocop/cop/naming/variable_number.rb +98 -8
- data/lib/rubocop/cop/offense.rb +3 -3
- data/lib/rubocop/cop/style/and_or.rb +1 -3
- data/lib/rubocop/cop/style/arguments_forwarding.rb +142 -0
- data/lib/rubocop/cop/style/bisected_attr_accessor.rb +0 -4
- data/lib/rubocop/cop/style/case_like_if.rb +0 -4
- data/lib/rubocop/cop/style/collection_compact.rb +91 -0
- data/lib/rubocop/cop/style/document_dynamic_eval_definition.rb +162 -0
- data/lib/rubocop/cop/style/double_negation.rb +6 -1
- data/lib/rubocop/cop/style/hash_syntax.rb +3 -3
- data/lib/rubocop/cop/style/identical_conditional_branches.rb +7 -2
- data/lib/rubocop/cop/style/if_inside_else.rb +37 -1
- data/lib/rubocop/cop/style/if_unless_modifier.rb +7 -3
- data/lib/rubocop/cop/style/infinite_loop.rb +4 -0
- data/lib/rubocop/cop/style/keyword_parameters_order.rb +12 -0
- data/lib/rubocop/cop/style/mixin_grouping.rb +0 -4
- data/lib/rubocop/cop/style/multiple_comparison.rb +55 -7
- data/lib/rubocop/cop/style/negated_if_else_condition.rb +104 -0
- data/lib/rubocop/cop/style/nil_lambda.rb +52 -0
- data/lib/rubocop/cop/style/raise_args.rb +21 -6
- data/lib/rubocop/cop/style/redundant_regexp_character_class.rb +7 -1
- data/lib/rubocop/cop/style/redundant_regexp_escape.rb +1 -1
- data/lib/rubocop/cop/style/semicolon.rb +3 -0
- data/lib/rubocop/cop/style/static_class.rb +97 -0
- data/lib/rubocop/cop/style/swap_values.rb +108 -0
- data/lib/rubocop/cop/style/while_until_modifier.rb +9 -0
- data/lib/rubocop/cop/team.rb +6 -1
- data/lib/rubocop/cop/util.rb +5 -1
- data/lib/rubocop/ext/regexp_node.rb +17 -9
- data/lib/rubocop/ext/regexp_parser.rb +84 -0
- data/lib/rubocop/formatter/formatter_set.rb +2 -1
- data/lib/rubocop/formatter/git_hub_actions_formatter.rb +47 -0
- data/lib/rubocop/magic_comment.rb +2 -2
- data/lib/rubocop/options.rb +2 -0
- data/lib/rubocop/rspec/shared_contexts.rb +4 -0
- data/lib/rubocop/target_ruby.rb +57 -1
- data/lib/rubocop/version.rb +1 -1
- metadata +21 -5
@@ -15,6 +15,20 @@ module RuboCop
|
|
15
15
|
# valid_attributes: %i[name content],
|
16
16
|
# nested_attributes: %i[name content %i[incorrectly nested]]
|
17
17
|
# }
|
18
|
+
#
|
19
|
+
# # good
|
20
|
+
#
|
21
|
+
# # Neither is incompatible with the bad case, but probably the intended code.
|
22
|
+
# attributes = {
|
23
|
+
# valid_attributes: %i[name content],
|
24
|
+
# nested_attributes: [:name, :content, %i[incorrectly nested]]
|
25
|
+
# }
|
26
|
+
#
|
27
|
+
# attributes = {
|
28
|
+
# valid_attributes: %i[name content],
|
29
|
+
# nested_attributes: [:name, :content, [:incorrectly, :nested]]
|
30
|
+
# }
|
31
|
+
#
|
18
32
|
class NestedPercentLiteral < Base
|
19
33
|
include PercentLiteral
|
20
34
|
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Lint
|
6
|
+
# Checks for the presence of a `return` inside a `begin..end` block
|
7
|
+
# in assignment contexts.
|
8
|
+
# In this situation, the `return` will result in an exit from the current
|
9
|
+
# method, possibly leading to unexpected behavior.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
#
|
13
|
+
# # bad
|
14
|
+
#
|
15
|
+
# @some_variable ||= begin
|
16
|
+
# return some_value if some_condition_is_met
|
17
|
+
#
|
18
|
+
# do_something
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
#
|
23
|
+
# # good
|
24
|
+
#
|
25
|
+
# @some_variable ||= begin
|
26
|
+
# if some_condition_is_met
|
27
|
+
# some_value
|
28
|
+
# else
|
29
|
+
# do_something
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# # good
|
34
|
+
#
|
35
|
+
# some_variable = if some_condition_is_met
|
36
|
+
# return if another_condition_is_met
|
37
|
+
#
|
38
|
+
# some_value
|
39
|
+
# else
|
40
|
+
# do_something
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
class NoReturnInBeginEndBlocks < Cop
|
44
|
+
MSG = 'Do not `return` in `begin..end` blocks in assignment contexts.'
|
45
|
+
|
46
|
+
def on_lvasgn(node)
|
47
|
+
node.each_node(:kwbegin) do |kwbegin_node|
|
48
|
+
kwbegin_node.each_node(:return) do |return_node|
|
49
|
+
add_offense(return_node)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
alias on_or_asgn on_lvasgn
|
54
|
+
alias on_op_asgn on_lvasgn
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -7,6 +7,17 @@ module RuboCop
|
|
7
7
|
# number conversion can cause unexpected error if auto type conversion
|
8
8
|
# fails. Cop prefer parsing with number class instead.
|
9
9
|
#
|
10
|
+
# Conversion with `Integer`, `Float`, etc. will raise an `ArgumentError`
|
11
|
+
# if given input that is not numeric (eg. an empty string), whereas
|
12
|
+
# `to_i`, etc. will try to convert regardless of input (`''.to_i => 0`).
|
13
|
+
# As such, this cop is disabled by default because it's not necessarily
|
14
|
+
# always correct to raise if a value is not numeric.
|
15
|
+
#
|
16
|
+
# NOTE: Some values cannot be converted properly using one of the `Kernel`
|
17
|
+
# method (for instance, `Time` and `DateTime` values are allowed by this
|
18
|
+
# cop by default). Similarly, Rails' duration methods do not work well
|
19
|
+
# with `Integer()` and can be ignored with `IgnoredMethods`.
|
20
|
+
#
|
10
21
|
# @example
|
11
22
|
#
|
12
23
|
# # bad
|
@@ -20,8 +31,19 @@ module RuboCop
|
|
20
31
|
# Integer('10', 10)
|
21
32
|
# Float('10.2')
|
22
33
|
# Complex('10')
|
34
|
+
#
|
35
|
+
# @example IgnoredMethods: [minutes]
|
36
|
+
#
|
37
|
+
# # good
|
38
|
+
# 10.minutes.to_i
|
39
|
+
#
|
40
|
+
# @example IgnoredClasses: [Time, DateTime] (default)
|
41
|
+
#
|
42
|
+
# # good
|
43
|
+
# Time.now.to_datetime.to_i
|
23
44
|
class NumberConversion < Base
|
24
45
|
extend AutoCorrector
|
46
|
+
include IgnoredMethods
|
25
47
|
|
26
48
|
CONVERSION_METHOD_CLASS_MAPPING = {
|
27
49
|
to_i: "#{Integer.name}(%<number_object>s, 10)",
|
@@ -38,13 +60,9 @@ module RuboCop
|
|
38
60
|
(send $_ ${:to_i :to_f :to_c})
|
39
61
|
PATTERN
|
40
62
|
|
41
|
-
def_node_matcher :datetime?, <<~PATTERN
|
42
|
-
(send (const {nil? (cbase)} {:Time :DateTime}) ...)
|
43
|
-
PATTERN
|
44
|
-
|
45
63
|
def on_send(node)
|
46
64
|
to_method(node) do |receiver, to_method|
|
47
|
-
next if receiver.nil? ||
|
65
|
+
next if receiver.nil? || ignore_receiver?(receiver)
|
48
66
|
|
49
67
|
message = format(
|
50
68
|
MSG,
|
@@ -60,18 +78,33 @@ module RuboCop
|
|
60
78
|
|
61
79
|
private
|
62
80
|
|
63
|
-
def
|
64
|
-
|
65
|
-
|
66
|
-
|
81
|
+
def correct_method(node, receiver)
|
82
|
+
format(CONVERSION_METHOD_CLASS_MAPPING[node.method_name],
|
83
|
+
number_object: receiver.source)
|
84
|
+
end
|
67
85
|
|
68
|
-
|
86
|
+
def ignore_receiver?(receiver)
|
87
|
+
if receiver.send_type? && ignored_method?(receiver.method_name)
|
88
|
+
true
|
89
|
+
elsif (receiver = top_receiver(receiver))
|
90
|
+
receiver.const_type? && ignored_class?(receiver.const_name)
|
91
|
+
else
|
92
|
+
false
|
69
93
|
end
|
70
94
|
end
|
71
95
|
|
72
|
-
def
|
73
|
-
|
74
|
-
|
96
|
+
def top_receiver(node)
|
97
|
+
receiver = node
|
98
|
+
receiver = receiver.receiver until receiver.receiver.nil?
|
99
|
+
receiver
|
100
|
+
end
|
101
|
+
|
102
|
+
def ignored_classes
|
103
|
+
cop_config.fetch('IgnoredClasses', [])
|
104
|
+
end
|
105
|
+
|
106
|
+
def ignored_class?(name)
|
107
|
+
ignored_classes.include?(name.to_s)
|
75
108
|
end
|
76
109
|
end
|
77
110
|
end
|
@@ -19,7 +19,7 @@ module RuboCop
|
|
19
19
|
# puts $1 # => foo
|
20
20
|
#
|
21
21
|
class OutOfRangeRegexpRef < Base
|
22
|
-
MSG = '
|
22
|
+
MSG = '$%<backref>s is out of range (%<count>s regexp capture %<group>s detected).'
|
23
23
|
|
24
24
|
REGEXP_RECEIVER_METHODS = %i[=~ === match].to_set.freeze
|
25
25
|
REGEXP_ARGUMENT_METHODS = %i[=~ match grep gsub gsub! sub sub! [] slice slice! index rindex
|
@@ -35,14 +35,13 @@ module RuboCop
|
|
35
35
|
check_regexp(node.children.first)
|
36
36
|
end
|
37
37
|
|
38
|
-
def
|
38
|
+
def after_send(node)
|
39
39
|
@valid_ref = nil
|
40
40
|
|
41
|
-
if node
|
42
|
-
check_regexp(node.receiver)
|
43
|
-
elsif node.first_argument&.regexp_type? \
|
44
|
-
&& REGEXP_ARGUMENT_METHODS.include?(node.method_name)
|
41
|
+
if regexp_first_argument?(node)
|
45
42
|
check_regexp(node.first_argument)
|
43
|
+
elsif regexp_receiver?(node)
|
44
|
+
check_regexp(node.receiver)
|
46
45
|
end
|
47
46
|
end
|
48
47
|
|
@@ -56,9 +55,16 @@ module RuboCop
|
|
56
55
|
|
57
56
|
def on_nth_ref(node)
|
58
57
|
backref, = *node
|
59
|
-
return if @valid_ref.nil?
|
58
|
+
return if @valid_ref.nil? || backref <= @valid_ref
|
59
|
+
|
60
|
+
message = format(
|
61
|
+
MSG,
|
62
|
+
backref: backref,
|
63
|
+
count: @valid_ref.zero? ? 'no' : @valid_ref,
|
64
|
+
group: @valid_ref == 1 ? 'group' : 'groups'
|
65
|
+
)
|
60
66
|
|
61
|
-
add_offense(node
|
67
|
+
add_offense(node, message: message)
|
62
68
|
end
|
63
69
|
|
64
70
|
private
|
@@ -73,6 +79,19 @@ module RuboCop
|
|
73
79
|
node.each_capture(named: false).count
|
74
80
|
end
|
75
81
|
end
|
82
|
+
|
83
|
+
def regexp_first_argument?(send_node)
|
84
|
+
send_node.first_argument&.regexp_type? \
|
85
|
+
&& REGEXP_ARGUMENT_METHODS.include?(send_node.method_name)
|
86
|
+
end
|
87
|
+
|
88
|
+
def regexp_receiver?(send_node)
|
89
|
+
send_node.receiver&.regexp_type?
|
90
|
+
end
|
91
|
+
|
92
|
+
def nth_ref_receiver?(send_node)
|
93
|
+
send_node.receiver&.nth_ref_type?
|
94
|
+
end
|
76
95
|
end
|
77
96
|
end
|
78
97
|
end
|
@@ -88,31 +88,34 @@ module RuboCop
|
|
88
88
|
begin_pos = reposition(source, begin_pos, -1)
|
89
89
|
end_pos = reposition(source, end_pos, 1)
|
90
90
|
|
91
|
-
|
92
|
-
if source[begin_pos - 1] == ','
|
93
|
-
:before
|
94
|
-
elsif source[end_pos] == ','
|
95
|
-
:after
|
96
|
-
else
|
97
|
-
:none
|
98
|
-
end
|
99
|
-
|
100
|
-
range_to_remove(begin_pos, end_pos, comma_pos, comment)
|
91
|
+
range_to_remove(begin_pos, end_pos, comment)
|
101
92
|
end
|
102
93
|
|
103
|
-
def range_to_remove(begin_pos, end_pos,
|
94
|
+
def range_to_remove(begin_pos, end_pos, comment)
|
104
95
|
start = comment_start(comment)
|
96
|
+
source = comment.loc.expression.source
|
105
97
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
range_between(start + begin_pos, start + end_pos + 1)
|
98
|
+
if source[begin_pos - 1] == ','
|
99
|
+
range_with_comma_before(start, begin_pos, end_pos)
|
100
|
+
elsif source[end_pos] == ','
|
101
|
+
range_with_comma_after(comment, start, begin_pos, end_pos)
|
111
102
|
else
|
112
103
|
range_between(start, comment.loc.expression.end_pos)
|
113
104
|
end
|
114
105
|
end
|
115
106
|
|
107
|
+
def range_with_comma_before(start, begin_pos, end_pos)
|
108
|
+
range_between(start + begin_pos - 1, start + end_pos)
|
109
|
+
end
|
110
|
+
|
111
|
+
# If the list of cops is comma-separated, but without a empty space after the comma,
|
112
|
+
# we should **not** remove the prepending empty space, thus begin_pos += 1
|
113
|
+
def range_with_comma_after(comment, start, begin_pos, end_pos)
|
114
|
+
begin_pos += 1 if comment.loc.expression.source[end_pos + 1] != ' '
|
115
|
+
|
116
|
+
range_between(start + begin_pos, start + end_pos + 1)
|
117
|
+
end
|
118
|
+
|
116
119
|
def all_or_name(name)
|
117
120
|
name == 'all' ? 'all cops' : name
|
118
121
|
end
|
@@ -140,11 +140,10 @@ module RuboCop
|
|
140
140
|
rescued_groups.each_cons(2).all? do |x, y|
|
141
141
|
if x.include?(Exception)
|
142
142
|
false
|
143
|
-
elsif y.include?(Exception)
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
# `nil`s
|
143
|
+
elsif y.include?(Exception) ||
|
144
|
+
# consider sorted if a group is empty or only contains
|
145
|
+
# `nil`s
|
146
|
+
x.none? || y.none?
|
148
147
|
true
|
149
148
|
else
|
150
149
|
(x <=> y || 0) <= 0
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Lint
|
6
|
+
# This cop ensures that `to_enum`/`enum_for`, called for the current method,
|
7
|
+
# has correct arguments.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# # bad
|
11
|
+
# def method(x, y = 1)
|
12
|
+
# return to_enum(__method__, x) # `y` is missing
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# # good
|
16
|
+
# def method(x, y = 1)
|
17
|
+
# return to_enum(__method__, x, y)
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# # bad
|
21
|
+
# def method(required:)
|
22
|
+
# return to_enum(:method, required: something) # `required` has incorrect value
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# # good
|
26
|
+
# def method(required:)
|
27
|
+
# return to_enum(:method, required: required)
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
class ToEnumArguments < Base
|
31
|
+
MSG = 'Ensure you correctly provided all the arguments.'
|
32
|
+
|
33
|
+
RESTRICT_ON_SEND = %i[to_enum enum_for].freeze
|
34
|
+
|
35
|
+
def_node_matcher :enum_conversion_call?, <<~PATTERN
|
36
|
+
(send {nil? self} {:to_enum :enum_for} $_ $...)
|
37
|
+
PATTERN
|
38
|
+
|
39
|
+
def_node_matcher :method_name?, <<~PATTERN
|
40
|
+
{(send nil? :__method__) (sym %1)}
|
41
|
+
PATTERN
|
42
|
+
|
43
|
+
def_node_matcher :passing_keyword_arg?, <<~PATTERN
|
44
|
+
(pair (sym %1) (lvar %1))
|
45
|
+
PATTERN
|
46
|
+
|
47
|
+
def on_send(node)
|
48
|
+
def_node = node.each_ancestor(:def, :defs).first
|
49
|
+
return unless def_node
|
50
|
+
|
51
|
+
enum_conversion_call?(node) do |method_node, arguments|
|
52
|
+
add_offense(node) unless method_name?(method_node, def_node.method_name) &&
|
53
|
+
arguments_match?(arguments, def_node)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def arguments_match?(arguments, def_node)
|
60
|
+
index = 0
|
61
|
+
|
62
|
+
def_node.arguments.reject(&:blockarg_type?).all? do |def_arg|
|
63
|
+
send_arg = arguments[index]
|
64
|
+
case def_arg.type
|
65
|
+
when :arg, :restarg, :optarg
|
66
|
+
index += 1
|
67
|
+
end
|
68
|
+
|
69
|
+
send_arg && argument_match?(send_arg, def_arg)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
74
|
+
def argument_match?(send_arg, def_arg)
|
75
|
+
def_arg_name = def_arg.children[0]
|
76
|
+
|
77
|
+
case def_arg.type
|
78
|
+
when :arg, :restarg
|
79
|
+
send_arg.source == def_arg.source
|
80
|
+
when :optarg
|
81
|
+
send_arg.source == def_arg_name.to_s
|
82
|
+
when :kwoptarg, :kwarg
|
83
|
+
send_arg.hash_type? &&
|
84
|
+
send_arg.pairs.any? { |pair| passing_keyword_arg?(pair, def_arg_name) }
|
85
|
+
when :kwrestarg
|
86
|
+
send_arg.each_child_node(:kwsplat).any? { |child| child.source == def_arg.source }
|
87
|
+
when :forward_arg
|
88
|
+
send_arg.forwarded_args_type?
|
89
|
+
end
|
90
|
+
end
|
91
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Lint
|
6
|
+
# Looks for `reduce` or `inject` blocks where the value returned (implicitly or
|
7
|
+
# explicitly) does not include the accumulator. A block is considered valid as
|
8
|
+
# long as at least one return value includes the accumulator.
|
9
|
+
#
|
10
|
+
# If the accumulator is not included in the return value, then the entire
|
11
|
+
# block will just return a transformation of the last element value, and
|
12
|
+
# could be rewritten as such without a loop.
|
13
|
+
#
|
14
|
+
# Also catches instances where an index of the accumulator is returned, as
|
15
|
+
# this may change the type of object being retained.
|
16
|
+
#
|
17
|
+
# NOTE: For the purpose of reducing false positives, this cop only flags
|
18
|
+
# returns in `reduce` blocks where the element is the only variable in
|
19
|
+
# the expression (since we will not be able to tell what other variables
|
20
|
+
# relate to via static analysis).
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
#
|
24
|
+
# # bad
|
25
|
+
# (1..4).reduce(0) do |acc, el|
|
26
|
+
# el * 2
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# # bad, may raise a NoMethodError after the first iteration
|
30
|
+
# %w(a b c).reduce({}) do |acc, letter|
|
31
|
+
# acc[letter] = true
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# # good
|
35
|
+
# (1..4).reduce(0) do |acc, el|
|
36
|
+
# acc + el * 2
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# # good, element is returned but modified using the accumulator
|
40
|
+
# values.reduce do |acc, el|
|
41
|
+
# el << acc
|
42
|
+
# el
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# # good, returns the accumulator instead of the index
|
46
|
+
# %w(a b c).reduce({}) do |acc, letter|
|
47
|
+
# acc[letter] = true
|
48
|
+
# acc
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# # good, at least one branch returns the accumulator
|
52
|
+
# values.reduce(nil) do |result, value|
|
53
|
+
# break result if something?
|
54
|
+
# value
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# # ignored as the return value cannot be determined
|
58
|
+
# enum.reduce do |acc, el|
|
59
|
+
# x = foo(acc, el)
|
60
|
+
# bar(x)
|
61
|
+
# end
|
62
|
+
class UnmodifiedReduceAccumulator < Base
|
63
|
+
MSG = 'Ensure the accumulator `%<accum>s` will be modified by `%<method>s`.'
|
64
|
+
MSG_INDEX = 'Do not return an element of the accumulator in `%<method>s`.'
|
65
|
+
|
66
|
+
def_node_matcher :reduce_with_block?, <<~PATTERN
|
67
|
+
(block (send _recv {:reduce :inject} ...) (args arg+) ...)
|
68
|
+
PATTERN
|
69
|
+
|
70
|
+
def_node_matcher :accumulator_index?, <<~PATTERN
|
71
|
+
(send (lvar %1) {:[] :[]=} ...)
|
72
|
+
PATTERN
|
73
|
+
|
74
|
+
def_node_search :element_modified?, <<~PATTERN
|
75
|
+
{
|
76
|
+
(send _receiver !{:[] :[]=} <`(lvar %1) `_ ...>) # method(el, ...)
|
77
|
+
(send (lvar %1) _message <{ivar gvar cvar lvar send} ...>) # el.method(...)
|
78
|
+
(lvasgn %1 _) # el = ...
|
79
|
+
(%RuboCop::AST::Node::SHORTHAND_ASSIGNMENTS (lvasgn %1) ... _) # el += ...
|
80
|
+
}
|
81
|
+
PATTERN
|
82
|
+
|
83
|
+
def_node_matcher :lvar_used?, <<~PATTERN
|
84
|
+
{
|
85
|
+
(lvar %1)
|
86
|
+
(lvasgn %1 ...)
|
87
|
+
(send (lvar %1) :<< ...)
|
88
|
+
(dstr (begin (lvar %1)))
|
89
|
+
(%RuboCop::AST::Node::SHORTHAND_ASSIGNMENTS (lvasgn %1))
|
90
|
+
}
|
91
|
+
PATTERN
|
92
|
+
|
93
|
+
def_node_search :expression_values, <<~PATTERN
|
94
|
+
{
|
95
|
+
(%RuboCop::AST::Node::VARIABLES $_)
|
96
|
+
(%RuboCop::AST::Node::EQUALS_ASSIGNMENTS $_ ...)
|
97
|
+
(send (%RuboCop::AST::Node::VARIABLES $_) :<< ...)
|
98
|
+
$(send _ _)
|
99
|
+
(dstr (begin {(%RuboCop::AST::Node::VARIABLES $_)}))
|
100
|
+
(%RuboCop::AST::Node::SHORTHAND_ASSIGNMENTS (%RuboCop::AST::Node::EQUALS_ASSIGNMENTS $_) ...)
|
101
|
+
}
|
102
|
+
PATTERN
|
103
|
+
|
104
|
+
def on_block(node)
|
105
|
+
return unless reduce_with_block?(node)
|
106
|
+
|
107
|
+
check_return_values(node)
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
# Return values in a block are either the value given to next,
|
113
|
+
# the last line of a multiline block, or the only line of the block
|
114
|
+
def return_values(block_body_node)
|
115
|
+
nodes = [block_body_node.begin_type? ? block_body_node.child_nodes.last : block_body_node]
|
116
|
+
|
117
|
+
block_body_node.each_descendant(:next, :break) do |n|
|
118
|
+
# Ignore `next`/`break` inside an inner block
|
119
|
+
next if n.each_ancestor(:block).first != block_body_node.parent
|
120
|
+
next unless n.first_argument
|
121
|
+
|
122
|
+
nodes << n.first_argument
|
123
|
+
end
|
124
|
+
|
125
|
+
nodes
|
126
|
+
end
|
127
|
+
|
128
|
+
def check_return_values(block_node)
|
129
|
+
return_values = return_values(block_node.body)
|
130
|
+
accumulator_name = block_arg_name(block_node, 0)
|
131
|
+
element_name = block_arg_name(block_node, 1)
|
132
|
+
message_opts = { method: block_node.method_name, accum: accumulator_name }
|
133
|
+
|
134
|
+
if (node = returned_accumulator_index(return_values, accumulator_name))
|
135
|
+
add_offense(node, message: format(MSG_INDEX, message_opts))
|
136
|
+
elsif potential_offense?(return_values, block_node.body, element_name, accumulator_name)
|
137
|
+
return_values.each do |return_val|
|
138
|
+
unless acceptable_return?(return_val, element_name)
|
139
|
+
add_offense(return_val, message: format(MSG, message_opts))
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def block_arg_name(node, index)
|
146
|
+
node.arguments[index].node_parts[0]
|
147
|
+
end
|
148
|
+
|
149
|
+
# Look for an index of the accumulator being returned
|
150
|
+
# This is always an offense, in order to try to catch potential exceptions
|
151
|
+
# due to type mismatches
|
152
|
+
def returned_accumulator_index(return_values, accumulator_name)
|
153
|
+
return_values.detect { |val| accumulator_index?(val, accumulator_name) }
|
154
|
+
end
|
155
|
+
|
156
|
+
def potential_offense?(return_values, block_body, element_name, accumulator_name)
|
157
|
+
!(element_modified?(block_body, element_name) ||
|
158
|
+
returns_accumulator_anywhere?(return_values, accumulator_name))
|
159
|
+
end
|
160
|
+
|
161
|
+
# If the accumulator is used in any return value, the node is acceptable since
|
162
|
+
# the accumulator has a chance to change each iteration
|
163
|
+
def returns_accumulator_anywhere?(return_values, accumulator_name)
|
164
|
+
return_values.any? { |node| lvar_used?(node, accumulator_name) }
|
165
|
+
end
|
166
|
+
|
167
|
+
# Determine if a return value is acceptable for the purposes of this cop
|
168
|
+
# If it is an expression containing the accumulator, it is acceptable
|
169
|
+
# Otherwise, it is only unacceptable if it contains the iterated element, since we
|
170
|
+
# otherwise do not have enough information to prevent false positives.
|
171
|
+
def acceptable_return?(return_val, element_name)
|
172
|
+
vars = expression_values(return_val).uniq
|
173
|
+
return true if vars.none? || (vars - [element_name]).any?
|
174
|
+
|
175
|
+
false
|
176
|
+
end
|
177
|
+
|
178
|
+
# Exclude `begin` nodes inside a `dstr` from being collected by `return_values`
|
179
|
+
def allowed_type?(parent_node)
|
180
|
+
!parent_node.dstr_type?
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|