rubocop 1.87.0 → 1.88.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/config/default.yml +75 -71
- data/config/obsoletion.yml +21 -1
- data/lib/rubocop/cli/command/auto_generate_config.rb +6 -0
- data/lib/rubocop/cop/base.rb +17 -2
- data/lib/rubocop/cop/bundler/gem_comment.rb +2 -2
- data/lib/rubocop/cop/internal_affairs/redundant_let_rubocop_config_new.rb +5 -3
- data/lib/rubocop/cop/layout/block_alignment.rb +41 -4
- data/lib/rubocop/cop/lint/ambiguous_assignment.rb +1 -11
- data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +1 -10
- data/lib/rubocop/cop/lint/circular_argument_reference.rb +1 -3
- data/lib/rubocop/cop/lint/debugger.rb +0 -1
- data/lib/rubocop/cop/lint/deprecated_constants.rb +1 -7
- data/lib/rubocop/cop/lint/empty_block.rb +3 -3
- data/lib/rubocop/cop/lint/ensure_return.rb +19 -1
- data/lib/rubocop/cop/lint/erb_new_arguments.rb +3 -1
- data/lib/rubocop/cop/lint/float_comparison.rb +1 -0
- data/lib/rubocop/cop/lint/incompatible_io_select_with_fiber_scheduler.rb +5 -1
- data/lib/rubocop/cop/lint/interpolation_check.rb +18 -3
- data/lib/rubocop/cop/lint/lambda_without_literal_block.rb +1 -1
- data/lib/rubocop/cop/lint/literal_assignment_in_condition.rb +11 -1
- data/lib/rubocop/cop/lint/literal_in_interpolation.rb +8 -11
- data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +4 -4
- data/lib/rubocop/cop/lint/no_return_in_begin_end_blocks.rb +16 -0
- data/lib/rubocop/cop/lint/non_local_exit_from_iterator.rb +1 -1
- data/lib/rubocop/cop/lint/number_conversion.rb +13 -4
- data/lib/rubocop/cop/lint/numeric_operation_with_constant_result.rb +3 -0
- data/lib/rubocop/cop/lint/ordered_magic_comments.rb +7 -7
- data/lib/rubocop/cop/lint/raise_exception.rb +1 -1
- data/lib/rubocop/cop/lint/rand_one.rb +1 -1
- data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +4 -1
- data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +4 -1
- data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +15 -4
- data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +14 -7
- data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +4 -0
- data/lib/rubocop/cop/lint/redundant_type_conversion.rb +7 -0
- data/lib/rubocop/cop/lint/redundant_with_index.rb +1 -1
- data/lib/rubocop/cop/lint/redundant_with_object.rb +5 -0
- data/lib/rubocop/cop/lint/refinement_import_methods.rb +8 -1
- data/lib/rubocop/cop/lint/regexp_as_condition.rb +9 -1
- data/lib/rubocop/cop/lint/require_parentheses.rb +13 -4
- data/lib/rubocop/cop/lint/require_range_parentheses.rb +2 -1
- data/lib/rubocop/cop/lint/require_relative_self_path.rb +5 -5
- data/lib/rubocop/cop/lint/rescue_type.rb +1 -1
- data/lib/rubocop/cop/lint/safe_navigation_chain.rb +1 -0
- data/lib/rubocop/cop/lint/safe_navigation_with_empty.rb +1 -1
- data/lib/rubocop/cop/lint/script_permission.rb +5 -1
- data/lib/rubocop/cop/lint/self_assignment.rb +24 -1
- data/lib/rubocop/cop/lint/send_with_mixin_argument.rb +1 -1
- data/lib/rubocop/cop/lint/shadowing_outer_local_variable.rb +14 -0
- data/lib/rubocop/cop/lint/shared_mutable_default.rb +3 -1
- data/lib/rubocop/cop/lint/suppressed_exception_in_number_conversion.rb +12 -0
- data/lib/rubocop/cop/lint/symbol_conversion.rb +21 -4
- data/lib/rubocop/cop/lint/to_enum_arguments.rb +28 -1
- data/lib/rubocop/cop/lint/top_level_return_with_argument.rb +1 -1
- data/lib/rubocop/cop/lint/trailing_comma_in_attribute_declaration.rb +4 -1
- data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +3 -1
- data/lib/rubocop/cop/lint/useless_assignment.rb +10 -5
- data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +7 -3
- data/lib/rubocop/cop/lint/useless_setter_call.rb +4 -1
- data/lib/rubocop/cop/lint/useless_times.rb +22 -1
- data/lib/rubocop/cop/metrics/collection_literal_length.rb +1 -1
- data/lib/rubocop/cop/style/alias.rb +1 -1
- data/lib/rubocop/cop/style/and_or.rb +1 -1
- data/lib/rubocop/cop/style/array_first_last.rb +12 -1
- data/lib/rubocop/cop/style/array_intersect.rb +4 -0
- data/lib/rubocop/cop/style/array_intersect_with_single_element.rb +3 -0
- data/lib/rubocop/cop/style/block_delimiters.rb +16 -2
- data/lib/rubocop/cop/style/case_equality.rb +14 -2
- data/lib/rubocop/cop/style/class_equality_comparison.rb +21 -13
- data/lib/rubocop/cop/style/class_methods_definitions.rb +11 -5
- data/lib/rubocop/cop/style/colon_method_call.rb +13 -6
- data/lib/rubocop/cop/style/combinable_loops.rb +5 -0
- data/lib/rubocop/cop/style/comparable_clamp.rb +12 -1
- data/lib/rubocop/cop/style/concat_array_literals.rb +5 -1
- data/lib/rubocop/cop/style/conditional_assignment.rb +6 -1
- data/lib/rubocop/cop/style/constant_visibility.rb +4 -1
- data/lib/rubocop/cop/style/date_time.rb +2 -2
- data/lib/rubocop/cop/style/dig_chain.rb +5 -0
- data/lib/rubocop/cop/style/fetch_env_var.rb +1 -1
- data/lib/rubocop/cop/style/file_write.rb +17 -14
- data/lib/rubocop/cop/style/hash_slice.rb +16 -0
- data/lib/rubocop/cop/style/if_unless_modifier.rb +1 -1
- data/lib/rubocop/cop/style/mutable_constant.rb +105 -11
- data/lib/rubocop/cop/style/parallel_assignment.rb +8 -1
- data/lib/rubocop/cop/style/redundant_format.rb +1 -0
- data/lib/rubocop/cop/style/semicolon.rb +16 -1
- data/lib/rubocop/cop/style/while_until_do.rb +7 -0
- data/lib/rubocop/cop/style/word_array.rb +1 -0
- data/lib/rubocop/cop/style/zero_length_predicate.rb +6 -3
- data/lib/rubocop/formatter/disabled_config_formatter.rb +14 -7
- data/lib/rubocop/server/core.rb +6 -0
- data/lib/rubocop/version.rb +1 -1
- metadata +3 -3
|
@@ -71,7 +71,7 @@ module RuboCop
|
|
|
71
71
|
node.each_child_node do |expr|
|
|
72
72
|
if expr.send_type?
|
|
73
73
|
correct_send(expr, corrector)
|
|
74
|
-
elsif expr.
|
|
74
|
+
elsif expr.type?(:return, :next, :break, :yield) || expr.assignment?
|
|
75
75
|
correct_other(expr, corrector)
|
|
76
76
|
end
|
|
77
77
|
end
|
|
@@ -40,8 +40,9 @@ module RuboCop
|
|
|
40
40
|
|
|
41
41
|
node = innermost_braces_node(node)
|
|
42
42
|
return if node.parent && brace_method?(node.parent)
|
|
43
|
+
return if compound_assignment_target?(node)
|
|
43
44
|
|
|
44
|
-
preferred = (value
|
|
45
|
+
preferred = preferred_name(value)
|
|
45
46
|
offense_range = find_offense_range(node)
|
|
46
47
|
|
|
47
48
|
add_offense(offense_range, message: format(MSG, preferred: preferred)) do |corrector|
|
|
@@ -53,6 +54,10 @@ module RuboCop
|
|
|
53
54
|
|
|
54
55
|
private
|
|
55
56
|
|
|
57
|
+
def preferred_name(value)
|
|
58
|
+
value.zero? ? 'first' : 'last'
|
|
59
|
+
end
|
|
60
|
+
|
|
56
61
|
def preferred_value(node, value)
|
|
57
62
|
value = ".#{value}" unless node.loc.dot
|
|
58
63
|
value
|
|
@@ -74,6 +79,12 @@ module RuboCop
|
|
|
74
79
|
def brace_method?(node)
|
|
75
80
|
node.send_type? && (node.method?(:[]) || node.method?(:[]=))
|
|
76
81
|
end
|
|
82
|
+
|
|
83
|
+
# `arr[0] += 1` etc. would autocorrect to `arr.first += 1`, which calls the
|
|
84
|
+
# nonexistent `first=`/`last=` setter and raises `NoMethodError`.
|
|
85
|
+
def compound_assignment_target?(node)
|
|
86
|
+
node.parent&.type?(:op_asgn, :or_asgn, :and_asgn) && node.parent.children.first == node
|
|
87
|
+
end
|
|
77
88
|
end
|
|
78
89
|
end
|
|
79
90
|
end
|
|
@@ -145,6 +145,10 @@ module RuboCop
|
|
|
145
145
|
|
|
146
146
|
dot = dot_node.loc.dot.source
|
|
147
147
|
bang = straight?(method_name) ? '' : '!'
|
|
148
|
+
# `a&.intersection(b)&.none?` returns `nil` when `a` is `nil`, but the negated
|
|
149
|
+
# rewrite `!a&.intersect?(b)` returns `true` there, flipping the result.
|
|
150
|
+
return if bang == '!' && dot == '&.'
|
|
151
|
+
|
|
148
152
|
replacement = "#{bang}#{receiver.source}#{dot}intersect?(#{argument.source})"
|
|
149
153
|
|
|
150
154
|
register_offense(node, replacement)
|
|
@@ -29,6 +29,9 @@ module RuboCop
|
|
|
29
29
|
def on_send(node)
|
|
30
30
|
array, element = single_element(node)
|
|
31
31
|
return unless array
|
|
32
|
+
# `[*foo]` is not a single element: the splat can expand to any number of
|
|
33
|
+
# elements, so `intersect?([*foo])` is not equivalent to `include?(*foo)`.
|
|
34
|
+
return if element.splat_type?
|
|
32
35
|
|
|
33
36
|
add_offense(
|
|
34
37
|
node.source_range.with(begin_pos: node.loc.selector.begin_pos)
|
|
@@ -389,9 +389,23 @@ module RuboCop
|
|
|
389
389
|
|
|
390
390
|
def require_do_end?(node)
|
|
391
391
|
return false if node.braces? || node.multiline?
|
|
392
|
-
return false unless (resbody = node.each_descendant(:resbody).first)
|
|
393
392
|
|
|
394
|
-
|
|
393
|
+
body = node.body
|
|
394
|
+
return false unless body
|
|
395
|
+
# `ensure` and a block-level `rescue` are illegal inside `{ }`; only a
|
|
396
|
+
# bare modifier rescue (`expr rescue expr`) can be written with braces.
|
|
397
|
+
return true if body.ensure_type?
|
|
398
|
+
return false unless body.rescue_type?
|
|
399
|
+
|
|
400
|
+
!modifier_rescue?(body)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def modifier_rescue?(rescue_node)
|
|
404
|
+
return false if rescue_node.body.nil? || rescue_node.else_branch
|
|
405
|
+
return false unless rescue_node.resbody_branches.one?
|
|
406
|
+
|
|
407
|
+
resbody = rescue_node.resbody_branches.first
|
|
408
|
+
resbody.exceptions.empty? && resbody.exception_variable.nil?
|
|
395
409
|
end
|
|
396
410
|
|
|
397
411
|
def braces_required_method?(method_name)
|
|
@@ -96,13 +96,25 @@ module RuboCop
|
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
def const_replacement(lhs, rhs)
|
|
99
|
-
"#{rhs
|
|
99
|
+
"#{parenthesize_if_needed(rhs)}.is_a?(#{lhs.source})"
|
|
100
100
|
end
|
|
101
101
|
|
|
102
102
|
def send_replacement(lhs, rhs)
|
|
103
103
|
return unless self_class?(lhs)
|
|
104
104
|
|
|
105
|
-
"#{rhs
|
|
105
|
+
"#{parenthesize_if_needed(rhs)}.is_a?(#{lhs.source})"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# `Array === a + b` must become `(a + b).is_a?(Array)`, not
|
|
109
|
+
# `a + b.is_a?(Array)` (which parses as `a + (b.is_a?(Array))`).
|
|
110
|
+
def parenthesize_if_needed(node)
|
|
111
|
+
requires_parentheses?(node) ? "(#{node.source})" : node.source
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def requires_parentheses?(node)
|
|
115
|
+
return true if node.type?(:and, :or, :if, :range) || node.assignment?
|
|
116
|
+
|
|
117
|
+
node.send_type? && (node.operator_method? || node.unary_operation?)
|
|
106
118
|
end
|
|
107
119
|
end
|
|
108
120
|
end
|
|
@@ -90,25 +90,33 @@ module RuboCop
|
|
|
90
90
|
private
|
|
91
91
|
|
|
92
92
|
def class_name(class_node, node)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
unless class_name_method?(node.children.first.method_name)
|
|
94
|
+
# `var.class == 'Foo'` compares a `Class` to a `String` (always false) and
|
|
95
|
+
# has no valid `instance_of?` rewrite, so don't suggest one.
|
|
96
|
+
return if class_node.str_type?
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
# When a variable or return value of a method is used, it returns nil
|
|
104
|
-
# because the type is not known and cannot be suggested.
|
|
105
|
-
return
|
|
106
|
-
end
|
|
98
|
+
return class_node.source
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
if (receiver = class_node.receiver) && class_name_method?(class_node.method_name)
|
|
102
|
+
return receiver.source
|
|
107
103
|
end
|
|
108
104
|
|
|
105
|
+
return string_class_name(class_node) if class_node.str_type?
|
|
106
|
+
# When a variable or return value of a method is used, the type is not known
|
|
107
|
+
# and cannot be suggested.
|
|
108
|
+
return if unable_to_determine_type?(class_node)
|
|
109
|
+
|
|
109
110
|
class_node.source
|
|
110
111
|
end
|
|
111
112
|
|
|
113
|
+
def string_class_name(class_node)
|
|
114
|
+
value = trim_string_quotes(class_node)
|
|
115
|
+
# Avoid `::::Foo` when the name is already fully qualified.
|
|
116
|
+
value.prepend('::') if require_cbase?(class_node) && !value.start_with?('::')
|
|
117
|
+
value
|
|
118
|
+
end
|
|
119
|
+
|
|
112
120
|
def class_name_method?(method_name)
|
|
113
121
|
CLASS_NAME_METHODS.include?(method_name)
|
|
114
122
|
end
|
|
@@ -140,15 +140,21 @@ module RuboCop
|
|
|
140
140
|
|
|
141
141
|
def extract_def_from_sclass(def_node, sclass_node)
|
|
142
142
|
range = source_range_with_comment(def_node)
|
|
143
|
-
source = range
|
|
144
|
-
"def #{def_node.method_name}",
|
|
145
|
-
"def self.#{def_node.method_name}"
|
|
146
|
-
)
|
|
147
|
-
|
|
143
|
+
source = prefix_def_with_self(range, def_node)
|
|
148
144
|
source = source.gsub(/^ {#{indentation_diff(def_node, sclass_node)}}/, '')
|
|
149
145
|
[range, source.chomp]
|
|
150
146
|
end
|
|
151
147
|
|
|
148
|
+
# Splice in `self.` at the actual `def` keyword rather than substituting the
|
|
149
|
+
# first textual `def <name>`, which may appear inside a preceding comment.
|
|
150
|
+
def prefix_def_with_self(range, def_node)
|
|
151
|
+
keyword_offset = def_node.loc.keyword.begin_pos - range.begin_pos
|
|
152
|
+
name_end_offset = def_node.loc.name.end_pos - range.begin_pos
|
|
153
|
+
source = range.source.dup
|
|
154
|
+
source[keyword_offset...name_end_offset] = "def self.#{def_node.method_name}"
|
|
155
|
+
source
|
|
156
|
+
end
|
|
157
|
+
|
|
152
158
|
def indentation_diff(node1, node2)
|
|
153
159
|
node1.loc.column - node2.loc.column
|
|
154
160
|
end
|
|
@@ -24,10 +24,9 @@ module RuboCop
|
|
|
24
24
|
|
|
25
25
|
MSG = 'Do not use `::` for method calls.'
|
|
26
26
|
|
|
27
|
-
# @!method
|
|
28
|
-
def_node_matcher :
|
|
29
|
-
(
|
|
30
|
-
(const nil? :Java) _)
|
|
27
|
+
# @!method java_root?(node)
|
|
28
|
+
def_node_matcher :java_root?, <<~PATTERN
|
|
29
|
+
(const nil? :Java)
|
|
31
30
|
PATTERN
|
|
32
31
|
|
|
33
32
|
def self.autocorrect_incompatible_with
|
|
@@ -37,11 +36,19 @@ module RuboCop
|
|
|
37
36
|
def on_send(node)
|
|
38
37
|
return unless node.receiver && node.double_colon?
|
|
39
38
|
return if node.camel_case_method?
|
|
40
|
-
# ignore Java interop code like Java::int
|
|
41
|
-
return if
|
|
39
|
+
# ignore Java interop code like `Java::int` or `Java::com::method`
|
|
40
|
+
return if java_interop?(node)
|
|
42
41
|
|
|
43
42
|
add_offense(node.loc.dot) { |corrector| corrector.replace(node.loc.dot, '.') }
|
|
44
43
|
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def java_interop?(node)
|
|
48
|
+
receiver = node.receiver
|
|
49
|
+
receiver = receiver.receiver while receiver.respond_to?(:receiver) && receiver.receiver
|
|
50
|
+
java_root?(receiver)
|
|
51
|
+
end
|
|
45
52
|
end
|
|
46
53
|
end
|
|
47
54
|
end
|
|
@@ -85,8 +85,13 @@ module RuboCop
|
|
|
85
85
|
def on_for(node)
|
|
86
86
|
return unless node.parent&.begin_type?
|
|
87
87
|
return unless same_collection_looping_for?(node, node.left_sibling)
|
|
88
|
+
return unless node.body && node.left_sibling.body
|
|
88
89
|
|
|
89
90
|
add_offense(node) do |corrector|
|
|
91
|
+
# Combining loops with different iteration variables would leave the second
|
|
92
|
+
# body referencing an undefined variable, so only autocorrect when they match.
|
|
93
|
+
next unless node.variable == node.left_sibling.variable
|
|
94
|
+
|
|
90
95
|
combine_with_left_sibling(corrector, node)
|
|
91
96
|
end
|
|
92
97
|
end
|
|
@@ -90,7 +90,7 @@ module RuboCop
|
|
|
90
90
|
max = if_body.source
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
-
prefer = "#{
|
|
93
|
+
prefer = "#{parenthesize_if_needed(else_body)}.clamp(#{min}, #{max})"
|
|
94
94
|
|
|
95
95
|
add_offense(node, message: format(MSG, prefer: prefer)) do |corrector|
|
|
96
96
|
autocorrect(corrector, node, prefer)
|
|
@@ -119,6 +119,17 @@ module RuboCop
|
|
|
119
119
|
|
|
120
120
|
(lhs.source == else_body && op == :<) || (rhs.source == else_body && op == :>)
|
|
121
121
|
end
|
|
122
|
+
|
|
123
|
+
# `a + b` must become `(a + b).clamp(low, high)`, not `a + b.clamp(low, high)`
|
|
124
|
+
# (which parses as `a + (b.clamp(low, high))`).
|
|
125
|
+
def parenthesize_if_needed(node)
|
|
126
|
+
if node.type?(:and, :or, :if, :range) || node.assignment? ||
|
|
127
|
+
(node.send_type? && (node.operator_method? || node.unary_operation?))
|
|
128
|
+
"(#{node.source})"
|
|
129
|
+
else
|
|
130
|
+
node.source
|
|
131
|
+
end
|
|
132
|
+
end
|
|
122
133
|
end
|
|
123
134
|
end
|
|
124
135
|
end
|
|
@@ -55,6 +55,10 @@ module RuboCop
|
|
|
55
55
|
next unless prefer
|
|
56
56
|
|
|
57
57
|
corrector.replace(offense, prefer)
|
|
58
|
+
elsif node.arguments.any? { |argument| argument.children.empty? }
|
|
59
|
+
# In-place bracket removal would leave dangling commas (e.g.
|
|
60
|
+
# `concat([], [b])` -> `push(, b)`), so rebuild the call instead.
|
|
61
|
+
corrector.replace(offense, preferred_method(node))
|
|
58
62
|
else
|
|
59
63
|
corrector.replace(node.loc.selector, 'push')
|
|
60
64
|
node.arguments.each do |argument|
|
|
@@ -75,7 +79,7 @@ module RuboCop
|
|
|
75
79
|
|
|
76
80
|
def preferred_method(node)
|
|
77
81
|
new_arguments =
|
|
78
|
-
node.arguments.
|
|
82
|
+
node.arguments.flat_map do |arg|
|
|
79
83
|
if arg.percent_literal?
|
|
80
84
|
arg.children.map { |child| child.value.inspect }
|
|
81
85
|
else
|
|
@@ -283,7 +283,10 @@ module RuboCop
|
|
|
283
283
|
|
|
284
284
|
_condition, *branches, else_branch = *assignment
|
|
285
285
|
|
|
286
|
-
|
|
286
|
+
# Use the node accessor rather than the raw destructured branch: for
|
|
287
|
+
# `x = unless cond; body; end` (no `else`) the parser puts `body` in the
|
|
288
|
+
# else slot, but `else_branch` correctly reports there is no `else` clause.
|
|
289
|
+
return unless assignment.else_branch
|
|
287
290
|
return if allowed_single_line?([*branches, else_branch])
|
|
288
291
|
|
|
289
292
|
add_offense(node, message: ASSIGN_TO_CONDITION_MSG) do |corrector|
|
|
@@ -657,6 +660,8 @@ module RuboCop
|
|
|
657
660
|
remove_whitespace_in_branches(corrector, branch, condition, column)
|
|
658
661
|
|
|
659
662
|
parent_keyword = branch.parent.loc.keyword
|
|
663
|
+
return if same_line?(parent_keyword, condition)
|
|
664
|
+
|
|
660
665
|
corrector.remove_preceding(parent_keyword, parent_keyword.column - column)
|
|
661
666
|
end
|
|
662
667
|
end
|
|
@@ -89,7 +89,10 @@ module RuboCop
|
|
|
89
89
|
|
|
90
90
|
arguments = arguments.first.children.first.to_a if arguments.first&.splat_type?
|
|
91
91
|
constant_values = arguments.map do |argument|
|
|
92
|
-
|
|
92
|
+
# `respond_to?(:value)` is too broad: `int`/`float` nodes respond to it
|
|
93
|
+
# but their value is a `Numeric`, which has no `to_sym` (e.g.
|
|
94
|
+
# `private_constant 42`). Only symbol/string arguments are real names.
|
|
95
|
+
argument.value.to_sym if argument.type?(:sym, :str)
|
|
93
96
|
end
|
|
94
97
|
|
|
95
98
|
constant_values.include?(node.name)
|
|
@@ -59,12 +59,12 @@ module RuboCop
|
|
|
59
59
|
|
|
60
60
|
# @!method historic_date?(node)
|
|
61
61
|
def_node_matcher :historic_date?, <<~PATTERN
|
|
62
|
-
(
|
|
62
|
+
(call _ _ _ (const (const {nil? (cbase)} :Date) _))
|
|
63
63
|
PATTERN
|
|
64
64
|
|
|
65
65
|
# @!method to_datetime?(node)
|
|
66
66
|
def_node_matcher :to_datetime?, <<~PATTERN
|
|
67
|
-
(call
|
|
67
|
+
(call !nil? :to_datetime)
|
|
68
68
|
PATTERN
|
|
69
69
|
|
|
70
70
|
def on_send(node)
|
|
@@ -79,6 +79,11 @@ module RuboCop
|
|
|
79
79
|
corrector.replace(range, replacement)
|
|
80
80
|
|
|
81
81
|
comments_in_range(node).reverse_each do |comment|
|
|
82
|
+
# Only relocate comments that the replacement destroys. A trailing
|
|
83
|
+
# comment after the chain survives in place, so moving it would
|
|
84
|
+
# duplicate it (and splitting the line drops the indentation).
|
|
85
|
+
next if comment.source_range.begin_pos >= range.end_pos
|
|
86
|
+
|
|
82
87
|
corrector.insert_before(node, "#{comment.source}\n")
|
|
83
88
|
end
|
|
84
89
|
end
|
|
@@ -70,7 +70,7 @@ module RuboCop
|
|
|
70
70
|
|
|
71
71
|
def allowed_var?(node)
|
|
72
72
|
env_key_node = node.children.last
|
|
73
|
-
env_key_node.str_type? && cop_config['
|
|
73
|
+
env_key_node.str_type? && cop_config['AllowedVariables'].include?(env_key_node.value)
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
def used_as_flag?(node)
|
|
@@ -104,30 +104,33 @@ module RuboCop
|
|
|
104
104
|
|
|
105
105
|
def replacement(mode, filename, content, write_node)
|
|
106
106
|
replacement = "#{write_method(mode)}(#{filename.source}, #{content.source})"
|
|
107
|
-
|
|
108
|
-
return replacement
|
|
107
|
+
heredocs = removed_heredocs(filename, content, write_node)
|
|
108
|
+
return replacement if heredocs.empty?
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
#{replacement}
|
|
112
|
-
#{heredoc_range(heredoc).source}
|
|
113
|
-
REPLACEMENT
|
|
110
|
+
[replacement, *heredocs.map { |heredoc| heredoc_range(heredoc).source }].join("\n")
|
|
114
111
|
end
|
|
115
112
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
113
|
+
# Heredocs opened in the arguments keep working in the replacement, but their
|
|
114
|
+
# bodies are lost when they lie within the replaced range, so they need to be
|
|
115
|
+
# restored after the replacement.
|
|
116
|
+
def removed_heredocs(filename, content, write_node)
|
|
117
|
+
[filename, content].flat_map { |argument| find_heredocs(argument) }
|
|
118
|
+
.select { |heredoc| removed?(heredoc, write_node) }
|
|
119
|
+
.sort_by { |heredoc| heredoc.loc.heredoc_body.begin_pos }
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
def heredoc_range(heredoc)
|
|
123
123
|
range_between(heredoc.loc.heredoc_body.begin_pos, heredoc.loc.heredoc_end.end_pos)
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
-
def
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
def find_heredocs(node)
|
|
127
|
+
[node, *node.each_descendant(:any_str)].select do |child|
|
|
128
|
+
child.respond_to?(:heredoc?) && child.heredoc?
|
|
129
|
+
end
|
|
130
|
+
end
|
|
129
131
|
|
|
130
|
-
|
|
132
|
+
def removed?(heredoc, write_node)
|
|
133
|
+
heredoc.loc.heredoc_end.end_pos <= write_node.source_range.end_pos
|
|
131
134
|
end
|
|
132
135
|
end
|
|
133
136
|
end
|
|
@@ -19,6 +19,22 @@ module RuboCop
|
|
|
19
19
|
# This cop is unsafe because it cannot be guaranteed that the receiver
|
|
20
20
|
# is a `Hash` or responds to the replacement method.
|
|
21
21
|
#
|
|
22
|
+
# Additionally, the replacement may change the order of the resulting
|
|
23
|
+
# hash: `Hash#slice` returns entries in the order the keys are given,
|
|
24
|
+
# whereas `select`, `filter`, and `reject` preserve the entry order of
|
|
25
|
+
# the receiver.
|
|
26
|
+
#
|
|
27
|
+
# For example:
|
|
28
|
+
#
|
|
29
|
+
# [source,ruby]
|
|
30
|
+
# ----
|
|
31
|
+
# hash = {foo: 1, bar: 2, baz: 3}
|
|
32
|
+
# keys = %i[baz foo]
|
|
33
|
+
#
|
|
34
|
+
# hash.select { |k, _v| keys.include?(k) } # => {foo: 1, baz: 3}
|
|
35
|
+
# hash.slice(*keys) # => {baz: 3, foo: 1}
|
|
36
|
+
# ----
|
|
37
|
+
#
|
|
22
38
|
# @example
|
|
23
39
|
#
|
|
24
40
|
# # bad
|
|
@@ -86,7 +86,7 @@ module RuboCop
|
|
|
86
86
|
MSG_USE_NORMAL = 'Modifier form of `%<keyword>s` makes the line too long.'
|
|
87
87
|
|
|
88
88
|
def self.autocorrect_incompatible_with
|
|
89
|
-
[Style::SoleNestedConditional]
|
|
89
|
+
[Style::Next, Style::SoleNestedConditional]
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
@@ -6,6 +6,14 @@ module RuboCop
|
|
|
6
6
|
# Checks whether some constant value isn't a
|
|
7
7
|
# mutable literal (e.g. array or hash).
|
|
8
8
|
#
|
|
9
|
+
# When the `Recursive` option is enabled, mutable literals nested inside
|
|
10
|
+
# arrays and hashes are also frozen, so an offense on the outermost
|
|
11
|
+
# unfrozen literal will autocorrect every nested mutable literal as well.
|
|
12
|
+
# When the outer literal already has `.freeze` appended, the cop descends
|
|
13
|
+
# into it and reports each outermost unfrozen literal underneath. The
|
|
14
|
+
# option is disabled by default to preserve existing behavior; opt in to
|
|
15
|
+
# get strict nested freezing.
|
|
16
|
+
#
|
|
9
17
|
# Strict mode can be used to freeze all constants, rather than
|
|
10
18
|
# just literals.
|
|
11
19
|
# Strict mode is considered an experimental feature. It has not been
|
|
@@ -49,6 +57,17 @@ module RuboCop
|
|
|
49
57
|
# CONST = Something.new
|
|
50
58
|
#
|
|
51
59
|
#
|
|
60
|
+
# @example Recursive: false (default)
|
|
61
|
+
# # good - only the outer container needs to be frozen
|
|
62
|
+
# CONST = [{ a: [], b: 'foo' }].freeze
|
|
63
|
+
#
|
|
64
|
+
# @example Recursive: true
|
|
65
|
+
# # bad - nested mutable literals must be frozen too
|
|
66
|
+
# CONST = [{ a: [], b: 'foo' }].freeze
|
|
67
|
+
#
|
|
68
|
+
# # good
|
|
69
|
+
# CONST = [{ a: [].freeze, b: 'foo'.freeze }.freeze].freeze
|
|
70
|
+
#
|
|
52
71
|
# @example EnforcedStyle: strict
|
|
53
72
|
# # bad
|
|
54
73
|
# CONST = Something.new
|
|
@@ -138,10 +157,30 @@ module RuboCop
|
|
|
138
157
|
private
|
|
139
158
|
|
|
140
159
|
def on_assignment(value)
|
|
141
|
-
|
|
142
|
-
|
|
160
|
+
nodes = mutable_nodes(value) do |node|
|
|
161
|
+
if style == :strict
|
|
162
|
+
strict_check(node)
|
|
163
|
+
else
|
|
164
|
+
literal_check(node)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
nodes.each do |node|
|
|
169
|
+
add_offense(node) { |corrector| autocorrect(corrector, node) }
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def mutable_nodes(value, &block)
|
|
174
|
+
if recursive? && explicitly_frozen_literal?(value)
|
|
175
|
+
literal_children(value.receiver).flat_map { |c| mutable_nodes(c, &block) }
|
|
143
176
|
else
|
|
144
|
-
|
|
177
|
+
node_offending = yield(value)
|
|
178
|
+
|
|
179
|
+
if node_offending
|
|
180
|
+
[value]
|
|
181
|
+
else
|
|
182
|
+
[]
|
|
183
|
+
end
|
|
145
184
|
end
|
|
146
185
|
end
|
|
147
186
|
|
|
@@ -151,18 +190,20 @@ module RuboCop
|
|
|
151
190
|
return if frozen_string_literal?(value)
|
|
152
191
|
return if shareable_constant_value?(value)
|
|
153
192
|
|
|
154
|
-
|
|
193
|
+
true
|
|
155
194
|
end
|
|
156
195
|
|
|
157
|
-
def
|
|
158
|
-
|
|
159
|
-
return unless mutable_literal?(value) ||
|
|
160
|
-
(target_ruby_version <= 2.7 && range_enclosed_in_parentheses)
|
|
161
|
-
|
|
196
|
+
def literal_check(value)
|
|
197
|
+
return unless mutable_or_unfrozen_range?(value)
|
|
162
198
|
return if frozen_string_literal?(value)
|
|
163
199
|
return if shareable_constant_value?(value)
|
|
164
200
|
|
|
165
|
-
|
|
201
|
+
true
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def mutable_or_unfrozen_range?(value)
|
|
205
|
+
mutable_literal?(value) ||
|
|
206
|
+
(target_ruby_version <= 2.7 && range_enclosed_in_parentheses?(value))
|
|
166
207
|
end
|
|
167
208
|
|
|
168
209
|
def autocorrect(corrector, node)
|
|
@@ -171,13 +212,66 @@ module RuboCop
|
|
|
171
212
|
splat_value = splat_value(node)
|
|
172
213
|
if splat_value
|
|
173
214
|
correct_splat_expansion(corrector, expr, splat_value)
|
|
174
|
-
|
|
215
|
+
corrector.insert_after(expr, '.freeze')
|
|
216
|
+
return
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
if node.array_type? && !node.bracketed?
|
|
175
220
|
corrector.wrap(expr, '[', ']')
|
|
176
221
|
elsif requires_parentheses?(node)
|
|
177
222
|
corrector.wrap(expr, '(', ')')
|
|
178
223
|
end
|
|
179
224
|
|
|
180
225
|
corrector.insert_after(expr, '.freeze')
|
|
226
|
+
|
|
227
|
+
freeze_nested_literals(corrector, node) if recursive?
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Recursively freezes every nested mutable literal inside an array or
|
|
231
|
+
# hash literal. Already-frozen subtrees are not re-frozen, but their
|
|
232
|
+
# children are still inspected for unfrozen literals deeper down.
|
|
233
|
+
def freeze_nested_literals(corrector, node)
|
|
234
|
+
literal_children(node).each do |child|
|
|
235
|
+
if explicitly_frozen_literal?(child)
|
|
236
|
+
freeze_nested_literals(corrector, child.receiver)
|
|
237
|
+
elsif freezable_nested_literal?(child)
|
|
238
|
+
autocorrect(corrector, child)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def freezable_nested_literal?(node)
|
|
244
|
+
return false if frozen_string_literal?(node)
|
|
245
|
+
return false if shareable_constant_value?(node)
|
|
246
|
+
|
|
247
|
+
mutable_literal?(node)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Returns the child literals of an array or hash node that may
|
|
251
|
+
# themselves need freezing. For hashes, both keys and values are
|
|
252
|
+
# included. Percent-literal arrays (e.g. `%w(a b)`) are skipped because
|
|
253
|
+
# `.freeze` cannot be appended to their contents.
|
|
254
|
+
def literal_children(node)
|
|
255
|
+
case node.type
|
|
256
|
+
when :array
|
|
257
|
+
return [] if node.percent_literal?
|
|
258
|
+
|
|
259
|
+
node.children
|
|
260
|
+
when :hash
|
|
261
|
+
node.children.flat_map { |child| child.pair_type? ? child.children : [] }
|
|
262
|
+
else
|
|
263
|
+
[]
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def explicitly_frozen_literal?(node)
|
|
268
|
+
return false unless node.send_type? && node.method?(:freeze)
|
|
269
|
+
|
|
270
|
+
node.receiver && mutable_literal?(node.receiver)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def recursive?
|
|
274
|
+
cop_config.fetch('Recursive', false)
|
|
181
275
|
end
|
|
182
276
|
|
|
183
277
|
def mutable_literal?(value)
|
|
@@ -38,7 +38,7 @@ module RuboCop
|
|
|
38
38
|
rhs_elements = Array(rhs).compact # edge case for one constant
|
|
39
39
|
|
|
40
40
|
return if allowed_lhs?(node.assignments) || allowed_rhs?(rhs) ||
|
|
41
|
-
allowed_masign?(node.assignments, rhs_elements)
|
|
41
|
+
allowed_masign?(node.assignments, rhs_elements) || contains_heredoc?(rhs)
|
|
42
42
|
|
|
43
43
|
range = node.source_range.begin.join(rhs.source_range.end)
|
|
44
44
|
|
|
@@ -77,6 +77,13 @@ module RuboCop
|
|
|
77
77
|
!node.array_type? || elements.any?(&:splat_type?)
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
# Autocorrection splits the assignment into single assignments on
|
|
81
|
+
# consecutive lines, which would put following assignments into the
|
|
82
|
+
# heredoc body unless the heredoc bodies were moved along.
|
|
83
|
+
def contains_heredoc?(node)
|
|
84
|
+
node.each_descendant(:any_str).any?(&:heredoc?)
|
|
85
|
+
end
|
|
86
|
+
|
|
80
87
|
def assignment_corrector(node, rhs, order)
|
|
81
88
|
if node.parent&.rescue_type?
|
|
82
89
|
_assignment, modifier = *node.parent
|