rubocop-rails 2.30.2 → 2.34.3

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +2 -1
  4. data/config/default.yml +110 -50
  5. data/lib/rubocop/cop/mixin/active_record_helper.rb +2 -2
  6. data/lib/rubocop/cop/mixin/active_record_migrations_helper.rb +2 -2
  7. data/lib/rubocop/cop/mixin/database_type_resolvable.rb +2 -2
  8. data/lib/rubocop/cop/mixin/enforce_superclass.rb +6 -1
  9. data/lib/rubocop/cop/mixin/index_method.rb +6 -1
  10. data/lib/rubocop/cop/rails/action_controller_flash_before_render.rb +4 -2
  11. data/lib/rubocop/cop/rails/arel_star.rb +5 -5
  12. data/lib/rubocop/cop/rails/delegate.rb +7 -4
  13. data/lib/rubocop/cop/rails/duplicate_association.rb +1 -1
  14. data/lib/rubocop/cop/rails/duplicate_scope.rb +2 -2
  15. data/lib/rubocop/cop/rails/eager_evaluation_log_message.rb +1 -3
  16. data/lib/rubocop/cop/rails/env.rb +57 -0
  17. data/lib/rubocop/cop/rails/env_local.rb +50 -26
  18. data/lib/rubocop/cop/rails/environment_comparison.rb +56 -48
  19. data/lib/rubocop/cop/rails/exit.rb +7 -4
  20. data/lib/rubocop/cop/rails/file_path.rb +2 -2
  21. data/lib/rubocop/cop/rails/find_by.rb +1 -1
  22. data/lib/rubocop/cop/rails/find_by_or_assignment_memoization.rb +124 -0
  23. data/lib/rubocop/cop/rails/helper_instance_variable.rb +16 -17
  24. data/lib/rubocop/cop/rails/http_status_name_consistency.rb +80 -0
  25. data/lib/rubocop/cop/rails/index_by.rb +9 -0
  26. data/lib/rubocop/cop/rails/index_with.rb +14 -0
  27. data/lib/rubocop/cop/rails/inverse_of.rb +7 -0
  28. data/lib/rubocop/cop/rails/order_arguments.rb +84 -0
  29. data/lib/rubocop/cop/rails/output.rb +4 -2
  30. data/lib/rubocop/cop/rails/output_safety.rb +3 -1
  31. data/lib/rubocop/cop/rails/pluck.rb +13 -4
  32. data/lib/rubocop/cop/rails/presence.rb +67 -18
  33. data/lib/rubocop/cop/rails/read_write_attribute.rb +1 -1
  34. data/lib/rubocop/cop/rails/redirect_back_or_to.rb +99 -0
  35. data/lib/rubocop/cop/rails/redundant_presence_validation_on_belongs_to.rb +3 -3
  36. data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +7 -2
  37. data/lib/rubocop/cop/rails/reflection_class_name.rb +2 -2
  38. data/lib/rubocop/cop/rails/relative_date_constant.rb +1 -1
  39. data/lib/rubocop/cop/rails/reversible_migration.rb +2 -1
  40. data/lib/rubocop/cop/rails/save_bang.rb +4 -4
  41. data/lib/rubocop/cop/rails/schema_comment.rb +1 -1
  42. data/lib/rubocop/cop/rails/select_map.rb +12 -4
  43. data/lib/rubocop/cop/rails/three_state_boolean_column.rb +1 -1
  44. data/lib/rubocop/cop/rails/time_zone.rb +3 -1
  45. data/lib/rubocop/cop/rails/transaction_exit_statement.rb +5 -2
  46. data/lib/rubocop/cop/rails/uniq_before_pluck.rb +1 -1
  47. data/lib/rubocop/cop/rails/where_exists.rb +5 -5
  48. data/lib/rubocop/cop/rails_cops.rb +5 -0
  49. data/lib/rubocop/rails/version.rb +1 -1
  50. data/lib/rubocop-rails.rb +0 -1
  51. metadata +13 -8
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Checks for usage of `Rails.env` which can be replaced with Feature Flags
7
+ #
8
+ # @example
9
+ #
10
+ # # bad
11
+ # Rails.env.production? || Rails.env.local?
12
+ #
13
+ # # good
14
+ # if FeatureFlag.enabled?(:new_feature)
15
+ # # new feature code
16
+ # end
17
+ #
18
+ class Env < Base
19
+ MSG = 'Use Feature Flags or config instead of `Rails.env`.'
20
+ RESTRICT_ON_SEND = %i[env].freeze
21
+ # This allow list is derived from:
22
+ # (Rails.env.methods - Object.instance_methods).select { |m| m.to_s.end_with?('?') }
23
+ # and then removing the environment specific methods like development?, test?, production?, local?
24
+ ALLOWED_LIST = Set.new(
25
+ %i[
26
+ unicode_normalized?
27
+ exclude?
28
+ empty?
29
+ acts_like_string?
30
+ include?
31
+ is_utf8?
32
+ casecmp?
33
+ match?
34
+ starts_with?
35
+ ends_with?
36
+ start_with?
37
+ end_with?
38
+ valid_encoding?
39
+ ascii_only?
40
+ between?
41
+ ]
42
+ ).freeze
43
+
44
+ def on_send(node)
45
+ return unless node.receiver&.const_name == 'Rails'
46
+
47
+ parent = node.parent
48
+ return unless parent.respond_to?(:predicate_method?) && parent.predicate_method?
49
+
50
+ return if ALLOWED_LIST.include?(parent.method_name)
51
+
52
+ add_offense(parent)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -24,45 +24,69 @@ module RuboCop
24
24
 
25
25
  minimum_target_rails_version 7.1
26
26
 
27
- # @!method rails_env_local_or?(node)
28
- def_node_matcher :rails_env_local_or?, <<~PATTERN
29
- (or
30
- (send (send (const {cbase nil? } :Rails) :env) $%LOCAL_ENVIRONMENTS)
31
- (send (send (const {cbase nil? } :Rails) :env) $%LOCAL_ENVIRONMENTS)
32
- )
27
+ # @!method rails_env_local?(node)
28
+ def_node_matcher :rails_env_local?, <<~PATTERN
29
+ (send (send (const {cbase nil? } :Rails) :env) $%LOCAL_ENVIRONMENTS)
33
30
  PATTERN
34
31
 
35
- # @!method rails_env_local_and?(node)
36
- def_node_matcher :rails_env_local_and?, <<~PATTERN
37
- (and
38
- (send
39
- (send (send (const {cbase nil? } :Rails) :env) $%LOCAL_ENVIRONMENTS)
40
- :!)
41
- (send
42
- (send (send (const {cbase nil? } :Rails) :env) $%LOCAL_ENVIRONMENTS)
43
- :!)
44
- )
32
+ # @!method not_rails_env_local?(node)
33
+ def_node_matcher :not_rails_env_local?, <<~PATTERN
34
+ (send #rails_env_local? :!)
45
35
  PATTERN
46
36
 
47
37
  def on_or(node)
48
- rails_env_local_or?(node) do |*environments|
49
- next unless environments.to_set == LOCAL_ENVIRONMENTS
38
+ lhs, rhs = *node.children
39
+ return unless rails_env_local?(rhs)
50
40
 
51
- add_offense(node) do |corrector|
52
- corrector.replace(node, 'Rails.env.local?')
53
- end
41
+ nodes = [rhs]
42
+
43
+ if rails_env_local?(lhs)
44
+ nodes << lhs
45
+ elsif lhs.or_type? && rails_env_local?(lhs.rhs)
46
+ nodes << lhs.rhs
47
+ end
48
+
49
+ return unless environments(nodes).to_set == LOCAL_ENVIRONMENTS
50
+
51
+ range = offense_range(nodes)
52
+ add_offense(range) do |corrector|
53
+ corrector.replace(range, 'Rails.env.local?')
54
54
  end
55
55
  end
56
56
 
57
57
  def on_and(node)
58
- rails_env_local_and?(node) do |*environments|
59
- next unless environments.to_set == LOCAL_ENVIRONMENTS
58
+ lhs, rhs = *node.children
59
+ return unless not_rails_env_local?(rhs)
60
+
61
+ nodes = [rhs]
62
+
63
+ if not_rails_env_local?(lhs)
64
+ nodes << lhs
65
+ elsif lhs.operator_keyword? && not_rails_env_local?(lhs.rhs)
66
+ nodes << lhs.rhs
67
+ end
60
68
 
61
- add_offense(node, message: MSG_NEGATED) do |corrector|
62
- corrector.replace(node, '!Rails.env.local?')
63
- end
69
+ return unless environments(nodes).to_set == LOCAL_ENVIRONMENTS
70
+
71
+ range = offense_range(nodes)
72
+ add_offense(range, message: MSG_NEGATED) do |corrector|
73
+ corrector.replace(range, '!Rails.env.local?')
64
74
  end
65
75
  end
76
+
77
+ private
78
+
79
+ def environments(nodes)
80
+ if nodes[0].method?(:!)
81
+ nodes.map { |node| node.receiver.method_name }
82
+ else
83
+ nodes.map(&:method_name)
84
+ end
85
+ end
86
+
87
+ def offense_range(nodes)
88
+ nodes[1].source_range.begin.join(nodes[0].source_range.end)
89
+ end
66
90
  end
67
91
  end
68
92
  end
@@ -3,12 +3,13 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Rails
6
- # Checks that Rails.env is compared using `.production?`-like
6
+ # Checks that `Rails.env` is compared using `.production?`-like
7
7
  # methods instead of equality against a string or symbol.
8
8
  #
9
9
  # @example
10
10
  # # bad
11
11
  # Rails.env == 'production'
12
+ # Rails.env.to_sym == :production
12
13
  #
13
14
  # # bad, always returns false
14
15
  # Rails.env == :test
@@ -18,26 +19,40 @@ module RuboCop
18
19
  class EnvironmentComparison < Base
19
20
  extend AutoCorrector
20
21
 
21
- MSG = 'Favor `%<bang>sRails.env.%<env>s?` over `%<source>s`.'
22
+ MSG = 'Favor `%<prefer>s` over `%<source>s`.'
22
23
 
23
24
  SYM_MSG = 'Do not compare `Rails.env` with a symbol, it will always evaluate to `false`.'
24
25
 
25
26
  RESTRICT_ON_SEND = %i[== !=].freeze
26
27
 
27
- def_node_matcher :comparing_str_env_with_rails_env_on_lhs?, <<~PATTERN
28
- (send
29
- (send (const {nil? cbase} :Rails) :env)
30
- {:== :!=}
31
- $str
32
- )
28
+ def_node_matcher :comparing_env_with_rails_env_on_lhs?, <<~PATTERN
29
+ {
30
+ (send
31
+ (send (const {nil? cbase} :Rails) :env)
32
+ {:== :!=}
33
+ $str
34
+ )
35
+ (send
36
+ (send (send (const {nil? cbase} :Rails) :env) :to_sym)
37
+ {:== :!=}
38
+ $sym
39
+ )
40
+ }
33
41
  PATTERN
34
42
 
35
- def_node_matcher :comparing_str_env_with_rails_env_on_rhs?, <<~PATTERN
36
- (send
37
- $str
38
- {:== :!=}
39
- (send (const {nil? cbase} :Rails) :env)
40
- )
43
+ def_node_matcher :comparing_env_with_rails_env_on_rhs?, <<~PATTERN
44
+ {
45
+ (send
46
+ $str
47
+ {:== :!=}
48
+ (send (const {nil? cbase} :Rails) :env)
49
+ )
50
+ (send
51
+ $sym
52
+ {:== :!=}
53
+ (send (send (const {nil? cbase} :Rails) :env) :to_sym)
54
+ )
55
+ }
41
56
  PATTERN
42
57
 
43
58
  def_node_matcher :comparing_sym_env_with_rails_env_on_lhs?, <<~PATTERN
@@ -56,59 +71,52 @@ module RuboCop
56
71
  )
57
72
  PATTERN
58
73
 
59
- def_node_matcher :content, <<~PATTERN
60
- ({str sym} $_)
61
- PATTERN
62
-
63
74
  def on_send(node)
64
- if (env_node = comparing_str_env_with_rails_env_on_lhs?(node) ||
65
- comparing_str_env_with_rails_env_on_rhs?(node))
66
- env, = *env_node
67
- bang = node.method?(:!=) ? '!' : ''
68
- message = format(MSG, bang: bang, env: env, source: node.source)
69
-
70
- add_offense(node, message: message) do |corrector|
71
- autocorrect(corrector, node)
72
- end
75
+ check_env_comparison_with_rails_env(node)
76
+ check_sym_env_comparison_with_rails_env(node)
77
+ end
78
+
79
+ private
80
+
81
+ def check_env_comparison_with_rails_env(node)
82
+ return unless comparing_env_with_rails_env_on_lhs?(node) || comparing_env_with_rails_env_on_rhs?(node)
83
+
84
+ replacement = build_predicate_method(node)
85
+ message = format(MSG, prefer: replacement, source: node.source)
86
+
87
+ add_offense(node, message: message) do |corrector|
88
+ corrector.replace(node, replacement)
73
89
  end
90
+ end
74
91
 
92
+ def check_sym_env_comparison_with_rails_env(node)
75
93
  return unless comparing_sym_env_with_rails_env_on_lhs?(node) || comparing_sym_env_with_rails_env_on_rhs?(node)
76
94
 
77
95
  add_offense(node, message: SYM_MSG) do |corrector|
78
- autocorrect(corrector, node)
96
+ replacement = build_predicate_method(node)
97
+ corrector.replace(node, replacement)
79
98
  end
80
99
  end
81
100
 
82
- private
101
+ def build_predicate_method(node)
102
+ bang = node.method?(:!=) ? '!' : ''
83
103
 
84
- def autocorrect(corrector, node)
85
- replacement = build_predicate_method(node)
104
+ receiver, argument = extract_receiver_and_argument(node)
105
+ receiver = receiver.receiver if receiver.method?(:to_sym)
86
106
 
87
- corrector.replace(node, replacement)
107
+ "#{bang}#{receiver.source}.#{argument.value}?"
88
108
  end
89
109
 
90
- def build_predicate_method(node)
110
+ def extract_receiver_and_argument(node)
91
111
  if rails_env_on_lhs?(node)
92
- build_predicate_method_for_rails_env_on_lhs(node)
112
+ [node.receiver, node.first_argument]
93
113
  else
94
- build_predicate_method_for_rails_env_on_rhs(node)
114
+ [node.first_argument, node.receiver]
95
115
  end
96
116
  end
97
117
 
98
118
  def rails_env_on_lhs?(node)
99
- comparing_str_env_with_rails_env_on_lhs?(node) || comparing_sym_env_with_rails_env_on_lhs?(node)
100
- end
101
-
102
- def build_predicate_method_for_rails_env_on_lhs(node)
103
- bang = node.method?(:!=) ? '!' : ''
104
-
105
- "#{bang}#{node.receiver.source}.#{content(node.first_argument)}?"
106
- end
107
-
108
- def build_predicate_method_for_rails_env_on_rhs(node)
109
- bang = node.method?(:!=) ? '!' : ''
110
-
111
- "#{bang}#{node.first_argument.source}.#{content(node.receiver)}?"
119
+ comparing_env_with_rails_env_on_lhs?(node) || comparing_sym_env_with_rails_env_on_lhs?(node)
112
120
  end
113
121
  end
114
122
  end
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Rails
6
- # Enforces that `exit` calls are not used within a rails app.
6
+ # Enforces that `exit` and `abort` calls are not used within a rails app.
7
7
  # Valid options are instead to raise an error, break, return, or some
8
8
  # other form of stopping execution of current request.
9
9
  #
@@ -26,12 +26,15 @@ module RuboCop
26
26
  class Exit < Base
27
27
  include ConfigurableEnforcedStyle
28
28
 
29
- MSG = 'Do not use `exit` in Rails applications.'
30
- RESTRICT_ON_SEND = %i[exit exit!].freeze
29
+ MSG = 'Do not use `%<current>s` in Rails applications.'
30
+ RESTRICT_ON_SEND = %i[exit exit! abort].freeze
31
31
  EXPLICIT_RECEIVERS = %i[Kernel Process].freeze
32
32
 
33
33
  def on_send(node)
34
- add_offense(node.loc.selector) if offending_node?(node)
34
+ return unless offending_node?(node)
35
+
36
+ message = format(MSG, current: node.method_name)
37
+ add_offense(node.loc.selector, message: message)
35
38
  end
36
39
 
37
40
  private
@@ -190,7 +190,7 @@ module RuboCop
190
190
  else
191
191
  replace_with_rails_root_join(corrector, rails_root_node, argument_source)
192
192
  end
193
- node.children[rails_root_index + 1..].each { |child| corrector.remove(child) }
193
+ node.children[(rails_root_index + 1)..].each { |child| corrector.remove(child) }
194
194
  end
195
195
 
196
196
  def autocorrect_extension_after_rails_root_join_in_dstr(corrector, node, rails_root_index, extension_node)
@@ -281,7 +281,7 @@ module RuboCop
281
281
  end
282
282
 
283
283
  def extract_rails_root_join_argument_source(node, rails_root_index)
284
- node.children[rails_root_index + 1..].map(&:source).join.delete_prefix(File::SEPARATOR)
284
+ node.children[(rails_root_index + 1)..].map(&:source).join.delete_prefix(File::SEPARATOR)
285
285
  end
286
286
 
287
287
  def extension_node?(node)
@@ -46,7 +46,7 @@ module RuboCop
46
46
  private
47
47
 
48
48
  def where_method?(receiver)
49
- return false unless receiver
49
+ return false if !receiver || receiver.any_block_type?
50
50
 
51
51
  receiver.respond_to?(:method?) && receiver.method?(:where)
52
52
  end
@@ -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
@@ -37,6 +37,9 @@ module RuboCop
37
37
  (numblock
38
38
  (call _ :to_h) $1
39
39
  (array $_ (lvar :_1)))
40
+ (itblock
41
+ (call _ :to_h) $:it
42
+ (array $_ (lvar :it)))
40
43
  }
41
44
  PATTERN
42
45
 
@@ -50,6 +53,9 @@ module RuboCop
50
53
  (numblock
51
54
  (call _ {:map :collect}) $1
52
55
  (array $_ (lvar :_1)))
56
+ (itblock
57
+ (call _ {:map :collect}) $:it
58
+ (array $_ (lvar :it)))
53
59
  }
54
60
  :to_h)
55
61
  PATTERN
@@ -66,6 +72,9 @@ module RuboCop
66
72
  (numblock
67
73
  (call _ {:map :collect}) $1
68
74
  (array $_ (lvar :_1)))
75
+ (itblock
76
+ (call _ {:map :collect}) $:it
77
+ (array $_ (lvar :it)))
69
78
  }
70
79
  )
71
80
  PATTERN