rubocop-rails 2.32.0 → 2.34.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +47 -3
  3. data/lib/rubocop/cop/mixin/active_record_helper.rb +1 -1
  4. data/lib/rubocop/cop/mixin/index_method.rb +4 -0
  5. data/lib/rubocop/cop/rails/action_controller_flash_before_render.rb +4 -2
  6. data/lib/rubocop/cop/rails/delegate.rb +4 -4
  7. data/lib/rubocop/cop/rails/duplicate_association.rb +1 -1
  8. data/lib/rubocop/cop/rails/duplicate_scope.rb +2 -2
  9. data/lib/rubocop/cop/rails/env.rb +57 -0
  10. data/lib/rubocop/cop/rails/env_local.rb +50 -26
  11. data/lib/rubocop/cop/rails/environment_comparison.rb +56 -48
  12. data/lib/rubocop/cop/rails/exit.rb +7 -4
  13. data/lib/rubocop/cop/rails/file_path.rb +2 -2
  14. data/lib/rubocop/cop/rails/find_by.rb +1 -1
  15. data/lib/rubocop/cop/rails/find_by_or_assignment_memoization.rb +124 -0
  16. data/lib/rubocop/cop/rails/helper_instance_variable.rb +16 -17
  17. data/lib/rubocop/cop/rails/http_status_name_consistency.rb +80 -0
  18. data/lib/rubocop/cop/rails/index_with.rb +5 -0
  19. data/lib/rubocop/cop/rails/inverse_of.rb +7 -0
  20. data/lib/rubocop/cop/rails/order_arguments.rb +84 -0
  21. data/lib/rubocop/cop/rails/output.rb +3 -0
  22. data/lib/rubocop/cop/rails/output_safety.rb +3 -1
  23. data/lib/rubocop/cop/rails/pluck.rb +6 -3
  24. data/lib/rubocop/cop/rails/presence.rb +67 -18
  25. data/lib/rubocop/cop/rails/read_write_attribute.rb +1 -1
  26. data/lib/rubocop/cop/rails/redirect_back_or_to.rb +99 -0
  27. data/lib/rubocop/cop/rails/redundant_presence_validation_on_belongs_to.rb +3 -3
  28. data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +1 -1
  29. data/lib/rubocop/cop/rails/save_bang.rb +2 -2
  30. data/lib/rubocop/cop/rails/transaction_exit_statement.rb +4 -1
  31. data/lib/rubocop/cop/rails/where_exists.rb +5 -5
  32. data/lib/rubocop/cop/rails_cops.rb +5 -0
  33. data/lib/rubocop/rails/version.rb +1 -1
  34. metadata +9 -4
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Avoid memoizing `find_by` results with `||=`.
7
+ #
8
+ # It is common to see code that attempts to memoize `find_by` result by `||=`,
9
+ # but `find_by` may return `nil`, in which case it is not memoized as intended.
10
+ #
11
+ # NOTE: Respecting the object shapes introduced in Ruby 3.2, instance variables used
12
+ # for memoization that are initialized at object creation are ignored.
13
+ #
14
+ # @safety
15
+ # This cop is unsafe because detected `find_by` may not be Active Record's method,
16
+ # or the code may have a different purpose than memoization.
17
+ #
18
+ # @example
19
+ # # bad - exclusively doing memoization
20
+ # def current_user
21
+ # @current_user ||= User.find_by(id: session[:user_id])
22
+ # end
23
+ #
24
+ # # good
25
+ # def current_user
26
+ # return @current_user if defined?(@current_user)
27
+ #
28
+ # @current_user = User.find_by(id: session[:user_id])
29
+ # end
30
+ #
31
+ # # bad - method contains other code
32
+ # def current_user
33
+ # @current_user ||= User.find_by(id: session[:user_id])
34
+ # @current_user.do_something
35
+ # end
36
+ #
37
+ # # good
38
+ # def current_user
39
+ # if defined?(@current_user)
40
+ # @current_user
41
+ # else
42
+ # @current_user = User.find_by(id: session[:user_id])
43
+ # end
44
+ # @current_user.do_something
45
+ # end
46
+ class FindByOrAssignmentMemoization < Base
47
+ extend AutoCorrector
48
+
49
+ MSG = 'Avoid memoizing `find_by` results with `||=`.'
50
+
51
+ RESTRICT_ON_SEND = %i[find_by].freeze
52
+
53
+ def_node_matcher :find_by_or_assignment_memoization, <<~PATTERN
54
+ (or_asgn
55
+ (ivasgn $_)
56
+ $(send _ :find_by ...)
57
+ )
58
+ PATTERN
59
+
60
+ # When a method body contains only memoization, the correction can be more succinct.
61
+ def on_def(node)
62
+ find_by_or_assignment_memoization(node.body) do |variable_name, find_by|
63
+ next if instance_variable_assigned?(variable_name)
64
+
65
+ add_offense(node.body) do |corrector|
66
+ corrector.replace(
67
+ node.body,
68
+ <<~RUBY.rstrip
69
+ return #{variable_name} if defined?(#{variable_name})
70
+
71
+ #{variable_name} = #{find_by.source}
72
+ RUBY
73
+ )
74
+
75
+ correct_to_regular_method_definition(corrector, node) if node.endless?
76
+ end
77
+ end
78
+ end
79
+
80
+ def on_send(node)
81
+ assignment_node = node.parent
82
+
83
+ find_by_or_assignment_memoization(assignment_node) do |variable_name, find_by|
84
+ next if assignment_node.each_ancestor(:if).any? || instance_variable_assigned?(variable_name)
85
+
86
+ add_offense(assignment_node) do |corrector|
87
+ corrector.replace(
88
+ assignment_node,
89
+ <<~RUBY.rstrip
90
+ if defined?(#{variable_name})
91
+ #{variable_name}
92
+ else
93
+ #{variable_name} = #{find_by.source}
94
+ end
95
+ RUBY
96
+ )
97
+ end
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def instance_variable_assigned?(instance_variable_name)
104
+ initialize_methods.any? do |def_node|
105
+ def_node.each_descendant(:ivasgn).any? do |asgn_node|
106
+ asgn_node.name == instance_variable_name
107
+ end
108
+ end
109
+ end
110
+
111
+ def initialize_methods
112
+ @initialize_methods ||= processed_source.ast.each_descendant(:def).select { |node| node.method?(:initialize) }
113
+ end
114
+
115
+ def correct_to_regular_method_definition(corrector, node)
116
+ range = node.loc.assignment.join(node.body.source_range.begin)
117
+
118
+ corrector.replace(range, "\n")
119
+ corrector.insert_after(node, "\nend")
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -13,7 +13,7 @@ module RuboCop
13
13
  # variable, consider moving the behavior elsewhere, for
14
14
  # example to a model, decorator or presenter.
15
15
  #
16
- # Provided that a class inherits `ActionView::Helpers::FormBuilder`,
16
+ # Provided that an instance variable belongs to a class,
17
17
  # an offense will not be registered.
18
18
  #
19
19
  # @example
@@ -28,38 +28,37 @@ module RuboCop
28
28
  # end
29
29
  #
30
30
  # # good
31
- # class MyFormBuilder < ActionView::Helpers::FormBuilder
32
- # @template.do_something
31
+ # module ButtonHelper
32
+ # class Welcome
33
+ # def initialize(text:)
34
+ # @text = text
35
+ # end
36
+ # end
37
+ #
38
+ # def welcome(**)
39
+ # render Welcome.new(**)
40
+ # end
33
41
  # end
42
+ #
34
43
  class HelperInstanceVariable < Base
35
44
  MSG = 'Do not use instance variables in helpers.'
36
45
 
37
- def_node_matcher :form_builder_class?, <<~PATTERN
38
- (const
39
- (const
40
- (const {nil? cbase} :ActionView) :Helpers) :FormBuilder)
41
- PATTERN
42
-
43
46
  def on_ivar(node)
44
- return if inherit_form_builder?(node)
47
+ return if instance_variable_belongs_to_class?(node)
45
48
 
46
49
  add_offense(node)
47
50
  end
48
51
 
49
52
  def on_ivasgn(node)
50
- return if node.parent.or_asgn_type? || inherit_form_builder?(node)
53
+ return if node.parent.or_asgn_type? || instance_variable_belongs_to_class?(node)
51
54
 
52
55
  add_offense(node.loc.name)
53
56
  end
54
57
 
55
58
  private
56
59
 
57
- def inherit_form_builder?(node)
58
- node.each_ancestor(:class) do |class_node|
59
- return true if form_builder_class?(class_node.parent_class)
60
- end
61
-
62
- false
60
+ def instance_variable_belongs_to_class?(node)
61
+ node.each_ancestor(:class).any?
63
62
  end
64
63
  end
65
64
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Enforces consistency by using the current HTTP status names.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # render json: { error: "Invalid data" }, status: :unprocessable_entity
11
+ # head :payload_too_large
12
+ #
13
+ # # good
14
+ # render json: { error: "Invalid data" }, status: :unprocessable_content
15
+ # head :content_too_large
16
+ #
17
+ class HttpStatusNameConsistency < Base
18
+ extend AutoCorrector
19
+
20
+ requires_gem 'rack', '>= 3.1.0'
21
+
22
+ RESTRICT_ON_SEND = %i[render redirect_to head assert_response assert_redirected_to].freeze
23
+
24
+ PREFERRED_STATUSES = {
25
+ unprocessable_entity: :unprocessable_content,
26
+ payload_too_large: :content_too_large
27
+ }.freeze
28
+
29
+ MSG = 'Prefer `:%<preferred>s` over `:%<current>s`.'
30
+
31
+ def_node_matcher :status_method_call, <<~PATTERN
32
+ {
33
+ (send nil? {:render :redirect_to} _ $hash)
34
+ (send nil? {:render :redirect_to} $hash)
35
+ (send nil? {:head :assert_response} $_ ...)
36
+ (send nil? :assert_redirected_to _ $hash ...)
37
+ (send nil? :assert_redirected_to $hash ...)
38
+ }
39
+ PATTERN
40
+
41
+ def_node_matcher :status_hash_value, <<~PATTERN
42
+ (hash <(pair (sym :status) $_) ...>)
43
+ PATTERN
44
+
45
+ def on_send(node)
46
+ status_method_call(node) do |status_node|
47
+ if status_node.hash_type?
48
+ # Handle hash arguments like { status: :unprocessable_entity }
49
+ status_hash_value(status_node) do |status_value|
50
+ check_status_name_consistency(status_value)
51
+ end
52
+ else
53
+ # Handle positional arguments like head :unprocessable_entity
54
+ check_status_name_consistency(status_node)
55
+ end
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def check_status_name_consistency(node)
62
+ if node.sym_type? && PREFERRED_STATUSES.key?(node.value)
63
+ current_status = node.value
64
+ preferred_status = PREFERRED_STATUSES[current_status]
65
+
66
+ message = format(MSG, current: current_status, preferred: preferred_status)
67
+
68
+ add_offense(node, message: message) do |corrector|
69
+ corrector.replace(node, ":#{preferred_status}")
70
+ end
71
+ else
72
+ node.children.each do |child|
73
+ check_status_name_consistency(child) if child.is_a?(Parser::AST::Node)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -8,6 +8,11 @@ module RuboCop
8
8
  # an enumerable into a hash where the keys are the original elements.
9
9
  # Rails provides the `index_with` method for this purpose.
10
10
  #
11
+ # @safety
12
+ # This cop is marked as unsafe autocorrection, because `nil.to_h` returns {}
13
+ # but `nil.with_index` throws `NoMethodError`. Therefore, autocorrection is not
14
+ # compatible if the receiver is nil.
15
+ #
11
16
  # @example
12
17
  # # bad
13
18
  # [1, 2, 3].each_with_object({}) { |el, h| h[el] = foo(el) }
@@ -178,6 +178,7 @@ module RuboCop
178
178
  (pair (sym :inverse_of) nil)
179
179
  PATTERN
180
180
 
181
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
181
182
  def on_send(node)
182
183
  recv, arguments = association_recv_arguments(node)
183
184
  return unless arguments
@@ -192,9 +193,11 @@ module RuboCop
192
193
  return unless scope?(arguments) || options_requiring_inverse_of?(options)
193
194
 
194
195
  return if options_contain_inverse_of?(options)
196
+ return if dynamic_options?(options) && options.none? { |opt| inverse_of_nil_option?(opt) }
195
197
 
196
198
  add_offense(node.loc.selector, message: message(options))
197
199
  end
200
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
198
201
 
199
202
  def scope?(arguments)
200
203
  !ignore_scopes? && arguments.any?(&:block_type?)
@@ -216,6 +219,10 @@ module RuboCop
216
219
  end
217
220
  end
218
221
 
222
+ def dynamic_options?(options)
223
+ options.any? { |option| option&.kwsplat_type? }
224
+ end
225
+
219
226
  def options_contain_inverse_of?(options)
220
227
  options.any? { |opt| inverse_of_option?(opt) }
221
228
  end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Prefer symbol arguments over strings in `order` method.
7
+ #
8
+ # @safety
9
+ # Cop is unsafe because the receiver might not be an Active Record query.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # User.order('name')
14
+ # User.order('name DESC')
15
+ #
16
+ # # good
17
+ # User.order(:name)
18
+ # User.order(name: :desc)
19
+ #
20
+ class OrderArguments < Base
21
+ extend AutoCorrector
22
+
23
+ MSG = 'Prefer `%<prefer>s` instead.'
24
+
25
+ RESTRICT_ON_SEND = %i[order].freeze
26
+
27
+ def_node_matcher :string_order, <<~PATTERN
28
+ (call _ :order (str $_value)+)
29
+ PATTERN
30
+
31
+ ORDER_EXPRESSION_REGEX = /\A(\w+) ?(asc|desc)?\z/i.freeze
32
+
33
+ def on_send(node)
34
+ return unless (current_expressions = string_order(node))
35
+ return unless (preferred_expressions = replacement(current_expressions))
36
+
37
+ offense_range = find_offense_range(node)
38
+ add_offense(offense_range, message: format(MSG, prefer: preferred_expressions)) do |corrector|
39
+ corrector.replace(offense_range, preferred_expressions)
40
+ end
41
+ end
42
+ alias on_csend on_send
43
+
44
+ private
45
+
46
+ def find_offense_range(node)
47
+ node.first_argument.source_range.join(node.last_argument.source_range)
48
+ end
49
+
50
+ def replacement(order_expressions)
51
+ order_arguments = order_expressions.flat_map { |expr| expr.split(',') }
52
+ order_arguments.map! { |arg| extract_column_and_direction(arg.strip) }
53
+
54
+ return if order_arguments.any?(&:nil?)
55
+ return if order_arguments.any? { |column_name, _| positional_column?(column_name) }
56
+
57
+ convert_to_preferred_arguments(order_arguments).join(', ')
58
+ end
59
+
60
+ def convert_to_preferred_arguments(order_expressions)
61
+ use_hash = false
62
+ order_expressions.map do |column, direction|
63
+ if direction == :asc && !use_hash
64
+ ":#{column}"
65
+ else
66
+ use_hash = true
67
+ "#{column}: :#{direction}"
68
+ end
69
+ end
70
+ end
71
+
72
+ def positional_column?(column_name)
73
+ column_name.match?(/\A\d+\z/)
74
+ end
75
+
76
+ def extract_column_and_direction(order_expression)
77
+ return unless (column, direction = ORDER_EXPRESSION_REGEX.match(order_expression)&.captures)
78
+
79
+ [column.downcase, direction&.downcase&.to_sym || :asc]
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -38,9 +38,11 @@ module RuboCop
38
38
  ...)
39
39
  PATTERN
40
40
 
41
+ # rubocop:disable Metrics/CyclomaticComplexity
41
42
  def on_send(node)
42
43
  return if node.parent&.call_type? || node.block_node
43
44
  return if !output?(node) && !io_output?(node)
45
+ return if node.arguments.any? { |arg| arg.type?(:hash, :block_pass) }
44
46
 
45
47
  range = offense_range(node)
46
48
 
@@ -48,6 +50,7 @@ module RuboCop
48
50
  corrector.replace(range, 'Rails.logger.debug')
49
51
  end
50
52
  end
53
+ # rubocop:enable Metrics/CyclomaticComplexity
51
54
 
52
55
  private
53
56
 
@@ -84,7 +84,9 @@ module RuboCop
84
84
  private
85
85
 
86
86
  def non_interpolated_string?(node)
87
- node.receiver&.str_type? && !node.receiver.dstr_type?
87
+ return false unless (receiver = node.receiver)
88
+
89
+ receiver.str_type? || (receiver.dstr_type? && receiver.children.all?(&:str_type?))
88
90
  end
89
91
 
90
92
  def looks_like_rails_html_safe?(node)
@@ -27,6 +27,9 @@ module RuboCop
27
27
  # end
28
28
  # ----
29
29
  #
30
+ # If a method call has no receiver, like `do_something { users.map { |user| user[:foo] }`,
31
+ # it is not considered part of an iteration and will be detected.
32
+ #
30
33
  # @safety
31
34
  # This cop is unsafe because model can use column aliases.
32
35
  #
@@ -59,9 +62,9 @@ module RuboCop
59
62
  (any_block (call _ {:map :collect}) $_argument (send lvar :[] $_key))
60
63
  PATTERN
61
64
 
62
- # rubocop:disable Metrics/AbcSize
65
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
63
66
  def on_block(node)
64
- return if node.each_ancestor(:any_block).any?
67
+ return if node.each_ancestor(:any_block).first&.receiver
65
68
 
66
69
  pluck_candidate?(node) do |argument, key|
67
70
  next if key.regexp_type? || !use_one_block_argument?(argument)
@@ -79,7 +82,7 @@ module RuboCop
79
82
  register_offense(node, key)
80
83
  end
81
84
  end
82
- # rubocop:enable Metrics/AbcSize
85
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
83
86
  alias on_numblock on_block
84
87
  alias on_itblock on_block
85
88
 
@@ -37,38 +37,65 @@ module RuboCop
37
37
  #
38
38
  # # good
39
39
  # a.presence || b
40
+ #
41
+ # @example
42
+ # # bad
43
+ # a.present? ? a.foo : nil
44
+ #
45
+ # # bad
46
+ # !a.present? ? nil : a.foo
47
+ #
48
+ # # bad
49
+ # a.blank? ? nil : a.foo
50
+ #
51
+ # # bad
52
+ # !a.blank? ? a.foo : nil
53
+ #
54
+ # # good
55
+ # a.presence&.foo
56
+ #
57
+ # # good
58
+ # a.present? ? a[1] : nil
59
+ #
60
+ # # good
61
+ # a[:key] = value if a.present?
62
+ #
63
+ # # good
64
+ # a.present? ? a > 1 : nil
65
+ # a <= 0 if a.present?
40
66
  class Presence < Base
41
67
  include RangeHelp
42
68
  extend AutoCorrector
43
69
 
44
70
  MSG = 'Use `%<prefer>s` instead of `%<current>s`.'
71
+ INDEX_ACCESS_METHODS = %i[[] []=].freeze
45
72
 
46
73
  def_node_matcher :redundant_receiver_and_other, <<~PATTERN
47
74
  {
48
75
  (if
49
- (send $_recv :present?)
50
- _recv
76
+ {(send $_recv :blank?) (send (send $_recv :present?) :!)}
51
77
  $!begin
78
+ _recv
52
79
  )
53
80
  (if
54
- (send $_recv :blank?)
55
- $!begin
81
+ {(send $_recv :present?) (send (send $_recv :blank?) :!)}
56
82
  _recv
83
+ $!begin
57
84
  )
58
85
  }
59
86
  PATTERN
60
87
 
61
- def_node_matcher :redundant_negative_receiver_and_other, <<~PATTERN
88
+ def_node_matcher :redundant_receiver_and_chain, <<~PATTERN
62
89
  {
63
90
  (if
64
- (send (send $_recv :present?) :!)
65
- $!begin
66
- _recv
91
+ {(send $_recv :blank?) (send (send $_recv :present?) :!)}
92
+ {nil? nil_type?}
93
+ $(send _recv ...)
67
94
  )
68
95
  (if
69
- (send (send $_recv :blank?) :!)
70
- _recv
71
- $!begin
96
+ {(send $_recv :present?) (send (send $_recv :blank?) :!)}
97
+ $(send _recv ...)
98
+ {nil? nil_type?}
72
99
  )
73
100
  }
74
101
  PATTERN
@@ -82,18 +109,26 @@ module RuboCop
82
109
  register_offense(node, receiver, other)
83
110
  end
84
111
 
85
- redundant_negative_receiver_and_other(node) do |receiver, other|
86
- return if ignore_other_node?(other) || receiver.nil?
112
+ redundant_receiver_and_chain(node) do |receiver, chain|
113
+ return if ignore_chain_node?(chain) || receiver.nil?
87
114
 
88
- register_offense(node, receiver, other)
115
+ register_chain_offense(node, receiver, chain)
89
116
  end
90
117
  end
91
118
 
92
119
  private
93
120
 
94
121
  def register_offense(node, receiver, other)
95
- add_offense(node, message: message(node, receiver, other)) do |corrector|
96
- corrector.replace(node, replacement(receiver, other, node.left_sibling))
122
+ replacement = replacement(receiver, other, node.left_sibling)
123
+ add_offense(node, message: message(node, replacement)) do |corrector|
124
+ corrector.replace(node, replacement)
125
+ end
126
+ end
127
+
128
+ def register_chain_offense(node, receiver, chain)
129
+ replacement = chain_replacement(receiver, chain, node.left_sibling)
130
+ add_offense(node, message: message(node, replacement)) do |corrector|
131
+ corrector.replace(node, replacement)
97
132
  end
98
133
  end
99
134
 
@@ -105,8 +140,12 @@ module RuboCop
105
140
  node&.type?(:if, :rescue, :while)
106
141
  end
107
142
 
108
- def message(node, receiver, other)
109
- prefer = replacement(receiver, other, node.left_sibling).gsub(/^\s*|\n/, '')
143
+ def ignore_chain_node?(node)
144
+ index_access_method?(node) || node.assignment? || node.arithmetic_operation? || node.comparison_method?
145
+ end
146
+
147
+ def message(node, replacement)
148
+ prefer = replacement.gsub(/^\s*|\n/, '')
110
149
  current = current(node).gsub(/^\s*|\n/, '')
111
150
  format(MSG, prefer: prefer, current: current)
112
151
  end
@@ -146,6 +185,16 @@ module RuboCop
146
185
  def method_range(node)
147
186
  range_between(node.source_range.begin_pos, node.first_argument.source_range.begin_pos - 1)
148
187
  end
188
+
189
+ def chain_replacement(receiver, chain, left_sibling)
190
+ replaced = "#{receiver.source}.presence&.#{chain.method_name}"
191
+ replaced += "(#{chain.arguments.map(&:source).join(', ')})" if chain.arguments?
192
+ left_sibling ? "(#{replaced})" : replaced
193
+ end
194
+
195
+ def index_access_method?(node)
196
+ INDEX_ACCESS_METHODS.include?(node.method_name)
197
+ end
149
198
  end
150
199
  end
151
200
  end
@@ -66,7 +66,7 @@ module RuboCop
66
66
  return false unless enclosing_method
67
67
 
68
68
  shadowing_method_name = first_arg.value.to_s
69
- shadowing_method_name << '=' if node.method?(:write_attribute)
69
+ shadowing_method_name += '=' if node.method?(:write_attribute)
70
70
  enclosing_method.method?(shadowing_method_name)
71
71
  end
72
72