rubocop-rails 2.8.0 → 2.10.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +18 -2
  4. data/config/default.yml +101 -5
  5. data/config/obsoletion.yml +7 -0
  6. data/lib/rubocop/cop/mixin/active_record_helper.rb +16 -3
  7. data/lib/rubocop/cop/mixin/enforce_superclass.rb +40 -0
  8. data/lib/rubocop/cop/mixin/index_method.rb +8 -11
  9. data/lib/rubocop/cop/rails/action_filter.rb +10 -14
  10. data/lib/rubocop/cop/rails/active_record_aliases.rb +13 -17
  11. data/lib/rubocop/cop/rails/active_record_callbacks_order.rb +17 -12
  12. data/lib/rubocop/cop/rails/active_record_override.rb +1 -1
  13. data/lib/rubocop/cop/rails/active_support_aliases.rb +12 -21
  14. data/lib/rubocop/cop/rails/after_commit_override.rb +9 -2
  15. data/lib/rubocop/cop/rails/application_controller.rb +3 -7
  16. data/lib/rubocop/cop/rails/application_job.rb +2 -1
  17. data/lib/rubocop/cop/rails/application_mailer.rb +2 -7
  18. data/lib/rubocop/cop/rails/application_record.rb +2 -7
  19. data/lib/rubocop/cop/rails/arel_star.rb +41 -0
  20. data/lib/rubocop/cop/rails/assert_not.rb +8 -10
  21. data/lib/rubocop/cop/rails/attribute_default_block_value.rb +90 -0
  22. data/lib/rubocop/cop/rails/belongs_to.rb +10 -19
  23. data/lib/rubocop/cop/rails/blank.rb +31 -27
  24. data/lib/rubocop/cop/rails/bulk_change_table.rb +1 -1
  25. data/lib/rubocop/cop/rails/content_tag.rb +33 -18
  26. data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +2 -1
  27. data/lib/rubocop/cop/rails/date.rb +10 -11
  28. data/lib/rubocop/cop/rails/default_scope.rb +11 -4
  29. data/lib/rubocop/cop/rails/delegate.rb +9 -9
  30. data/lib/rubocop/cop/rails/delegate_allow_blank.rb +7 -8
  31. data/lib/rubocop/cop/rails/dynamic_find_by.rb +15 -12
  32. data/lib/rubocop/cop/rails/enum_hash.rb +11 -10
  33. data/lib/rubocop/cop/rails/enum_uniqueness.rb +2 -1
  34. data/lib/rubocop/cop/rails/environment_comparison.rb +18 -14
  35. data/lib/rubocop/cop/rails/environment_variable_access.rb +67 -0
  36. data/lib/rubocop/cop/rails/exit.rb +4 -10
  37. data/lib/rubocop/cop/rails/file_path.rb +6 -7
  38. data/lib/rubocop/cop/rails/find_by.rb +13 -13
  39. data/lib/rubocop/cop/rails/find_by_id.rb +12 -21
  40. data/lib/rubocop/cop/rails/find_each.rb +19 -18
  41. data/lib/rubocop/cop/rails/has_and_belongs_to_many.rb +3 -2
  42. data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +37 -6
  43. data/lib/rubocop/cop/rails/helper_instance_variable.rb +29 -3
  44. data/lib/rubocop/cop/rails/http_positional_arguments.rb +32 -21
  45. data/lib/rubocop/cop/rails/http_status.rb +7 -9
  46. data/lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb +8 -6
  47. data/lib/rubocop/cop/rails/index_by.rb +3 -2
  48. data/lib/rubocop/cop/rails/index_with.rb +3 -2
  49. data/lib/rubocop/cop/rails/inquiry.rb +4 -3
  50. data/lib/rubocop/cop/rails/inverse_of.rb +3 -2
  51. data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +17 -15
  52. data/lib/rubocop/cop/rails/link_to_blank.rb +25 -23
  53. data/lib/rubocop/cop/rails/mailer_name.rb +19 -13
  54. data/lib/rubocop/cop/rails/match_route.rb +14 -13
  55. data/lib/rubocop/cop/rails/negate_include.rb +10 -8
  56. data/lib/rubocop/cop/rails/not_null_column.rb +2 -1
  57. data/lib/rubocop/cop/rails/order_by_id.rb +1 -2
  58. data/lib/rubocop/cop/rails/output.rb +5 -2
  59. data/lib/rubocop/cop/rails/output_safety.rb +3 -2
  60. data/lib/rubocop/cop/rails/pick.rb +14 -12
  61. data/lib/rubocop/cop/rails/pluck.rb +6 -9
  62. data/lib/rubocop/cop/rails/pluck_id.rb +4 -6
  63. data/lib/rubocop/cop/rails/pluck_in_where.rb +7 -7
  64. data/lib/rubocop/cop/rails/pluralization_grammar.rb +10 -14
  65. data/lib/rubocop/cop/rails/presence.rb +12 -13
  66. data/lib/rubocop/cop/rails/present.rb +30 -24
  67. data/lib/rubocop/cop/rails/rake_environment.rb +8 -10
  68. data/lib/rubocop/cop/rails/read_write_attribute.rb +12 -11
  69. data/lib/rubocop/cop/rails/redundant_allow_nil.rb +29 -31
  70. data/lib/rubocop/cop/rails/redundant_foreign_key.rb +9 -12
  71. data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +11 -10
  72. data/lib/rubocop/cop/rails/reflection_class_name.rb +17 -3
  73. data/lib/rubocop/cop/rails/refute_methods.rb +9 -10
  74. data/lib/rubocop/cop/rails/relative_date_constant.rb +30 -21
  75. data/lib/rubocop/cop/rails/render_inline.rb +2 -1
  76. data/lib/rubocop/cop/rails/render_plain_text.rb +9 -14
  77. data/lib/rubocop/cop/rails/request_referer.rb +7 -7
  78. data/lib/rubocop/cop/rails/require_dependency.rb +38 -0
  79. data/lib/rubocop/cop/rails/reversible_migration.rb +4 -8
  80. data/lib/rubocop/cop/rails/reversible_migration_method_definition.rb +75 -0
  81. data/lib/rubocop/cop/rails/safe_navigation.rb +30 -11
  82. data/lib/rubocop/cop/rails/safe_navigation_with_blank.rb +5 -10
  83. data/lib/rubocop/cop/rails/save_bang.rb +17 -20
  84. data/lib/rubocop/cop/rails/scope_args.rb +2 -1
  85. data/lib/rubocop/cop/rails/short_i18n.rb +7 -9
  86. data/lib/rubocop/cop/rails/skips_model_validations.rb +4 -4
  87. data/lib/rubocop/cop/rails/squished_sql_heredocs.rb +5 -6
  88. data/lib/rubocop/cop/rails/time_zone.rb +35 -25
  89. data/lib/rubocop/cop/rails/time_zone_assignment.rb +37 -0
  90. data/lib/rubocop/cop/rails/uniq_before_pluck.rb +4 -6
  91. data/lib/rubocop/cop/rails/unique_validation_without_index.rb +4 -2
  92. data/lib/rubocop/cop/rails/unknown_env.rb +3 -3
  93. data/lib/rubocop/cop/rails/validation.rb +15 -14
  94. data/lib/rubocop/cop/rails/where_equals.rb +98 -0
  95. data/lib/rubocop/cop/rails/where_exists.rb +19 -13
  96. data/lib/rubocop/cop/rails/where_not.rb +14 -19
  97. data/lib/rubocop/cop/rails_cops.rb +8 -0
  98. data/lib/rubocop/rails.rb +2 -0
  99. data/lib/rubocop/rails/schema_loader.rb +4 -4
  100. data/lib/rubocop/rails/schema_loader/schema.rb +2 -4
  101. data/lib/rubocop/rails/version.rb +5 -1
  102. metadata +29 -14
@@ -7,7 +7,7 @@ module RuboCop
7
7
  # validations which are listed in
8
8
  # https://guides.rubyonrails.org/active_record_validations.html#skipping-validations
9
9
  #
10
- # Methods may be ignored from this rule by configuring a `Whitelist`.
10
+ # Methods may be ignored from this rule by configuring a `AllowedMethods`.
11
11
  #
12
12
  # @example
13
13
  # # bad
@@ -26,7 +26,7 @@ module RuboCop
26
26
  # user.update(website: 'example.com')
27
27
  # FileUtils.touch('file')
28
28
  #
29
- # @example Whitelist: ["touch"]
29
+ # @example AllowedMethods: ["touch"]
30
30
  # # bad
31
31
  # DiscussionBoard.decrement_counter(:post_count, 5)
32
32
  # DiscussionBoard.increment_counter(:post_count, 5)
@@ -35,7 +35,7 @@ module RuboCop
35
35
  # # good
36
36
  # user.touch
37
37
  #
38
- class SkipsModelValidations < Cop
38
+ class SkipsModelValidations < Base
39
39
  MSG = 'Avoid using `%<method>s` because it skips validations.'
40
40
 
41
41
  METHODS_WITH_ARGUMENTS = %w[decrement!
@@ -76,7 +76,7 @@ module RuboCop
76
76
  return if good_touch?(node)
77
77
  return if good_insert?(node)
78
78
 
79
- add_offense(node, location: :selector)
79
+ add_offense(node.loc.selector, message: message(node))
80
80
  end
81
81
  alias on_csend on_send
82
82
 
@@ -5,6 +5,8 @@ module RuboCop
5
5
  module Rails
6
6
  #
7
7
  # Checks SQL heredocs to use `.squish`.
8
+ # Some SQL syntax (e.g. PostgreSQL comments and functions) requires newlines
9
+ # to be preserved in order to work, thus auto-correction for this cop is not safe.
8
10
  #
9
11
  # @example
10
12
  # # bad
@@ -37,8 +39,9 @@ module RuboCop
37
39
  # WHERE post_id = 1
38
40
  # SQL
39
41
  #
40
- class SquishedSQLHeredocs < Cop
42
+ class SquishedSQLHeredocs < Base
41
43
  include Heredoc
44
+ extend AutoCorrector
42
45
 
43
46
  SQL = 'SQL'
44
47
  SQUISH = '.squish'
@@ -47,11 +50,7 @@ module RuboCop
47
50
  def on_heredoc(node)
48
51
  return unless offense_detected?(node)
49
52
 
50
- add_offense(node)
51
- end
52
-
53
- def autocorrect(node)
54
- lambda do |corrector|
53
+ add_offense(node) do |corrector|
55
54
  corrector.insert_after(node, SQUISH)
56
55
  end
57
56
  end
@@ -19,7 +19,7 @@ module RuboCop
19
19
  #
20
20
  # # bad
21
21
  # Time.now
22
- # Time.parse('2015-03-02 19:05:37')
22
+ # Time.parse('2015-03-02T19:05:37')
23
23
  #
24
24
  # # bad
25
25
  # Time.current
@@ -27,24 +27,26 @@ module RuboCop
27
27
  #
28
28
  # # good
29
29
  # Time.zone.now
30
- # Time.zone.parse('2015-03-02 19:05:37')
30
+ # Time.zone.parse('2015-03-02T19:05:37')
31
+ # Time.zone.parse('2015-03-02T19:05:37Z') # Respect ISO 8601 format with timezone specifier.
31
32
  #
32
33
  # @example EnforcedStyle: flexible (default)
33
34
  # # `flexible` allows usage of `in_time_zone` instead of `zone`.
34
35
  #
35
36
  # # bad
36
37
  # Time.now
37
- # Time.parse('2015-03-02 19:05:37')
38
+ # Time.parse('2015-03-02T19:05:37')
38
39
  #
39
40
  # # good
40
41
  # Time.zone.now
41
- # Time.zone.parse('2015-03-02 19:05:37')
42
+ # Time.zone.parse('2015-03-02T19:05:37')
42
43
  #
43
44
  # # good
44
45
  # Time.current
45
46
  # Time.at(timestamp).in_time_zone
46
- class TimeZone < Cop
47
+ class TimeZone < Base
47
48
  include ConfigurableEnforcedStyle
49
+ extend AutoCorrector
48
50
 
49
51
  MSG = 'Do not use `%<current>s` without zone. Use `%<prefer>s` ' \
50
52
  'instead.'
@@ -62,6 +64,8 @@ module RuboCop
62
64
  ACCEPTED_METHODS = %i[in_time_zone utc getlocal xmlschema iso8601
63
65
  jisx0301 rfc3339 httpdate to_i to_f].freeze
64
66
 
67
+ TIMEZONE_SPECIFIER = /[A-z]/.freeze
68
+
65
69
  def on_const(node)
66
70
  mod, klass = *node
67
71
  # we should only check core classes
@@ -71,26 +75,24 @@ module RuboCop
71
75
  check_time_node(klass, node.parent) if klass == :Time
72
76
  end
73
77
 
74
- def autocorrect(node)
75
- lambda do |corrector|
76
- # add `.zone`: `Time.at` => `Time.zone.at`
77
- corrector.insert_after(node.children[0].source_range, '.zone')
78
+ private
78
79
 
79
- case node.method_name
80
- when :current
81
- # replace `Time.zone.current` => `Time.zone.now`
82
- corrector.replace(node.loc.selector, 'now')
83
- when :new
84
- autocorrect_time_new(node, corrector)
85
- end
80
+ def autocorrect(corrector, node)
81
+ # add `.zone`: `Time.at` => `Time.zone.at`
82
+ corrector.insert_after(node.children[0].source_range, '.zone')
86
83
 
87
- # prefer `Time` over `DateTime` class
88
- corrector.replace(node.children.first.source_range, 'Time') if strict?
89
- remove_redundant_in_time_zone(corrector, node)
84
+ case node.method_name
85
+ when :current
86
+ # replace `Time.zone.current` => `Time.zone.now`
87
+ corrector.replace(node.loc.selector, 'now')
88
+ when :new
89
+ autocorrect_time_new(node, corrector)
90
90
  end
91
- end
92
91
 
93
- private
92
+ # prefer `Time` over `DateTime` class
93
+ corrector.replace(node.children.first.source_range, 'Time') if strict?
94
+ remove_redundant_in_time_zone(corrector, node)
95
+ end
94
96
 
95
97
  def autocorrect_time_new(node, corrector)
96
98
  if node.arguments?
@@ -117,9 +119,10 @@ module RuboCop
117
119
  end
118
120
 
119
121
  def check_time_node(klass, node)
122
+ return if attach_timezone_specifier?(node.first_argument)
123
+
120
124
  chain = extract_method_chain(node)
121
125
  return if not_danger_chain?(chain)
122
-
123
126
  return check_localtime(node) if need_check_localtime?(chain)
124
127
 
125
128
  method_name = (chain & DANGEROUS_METHODS).join('.')
@@ -128,7 +131,13 @@ module RuboCop
128
131
 
129
132
  message = build_message(klass, method_name, node)
130
133
 
131
- add_offense(node, location: :selector, message: message)
134
+ add_offense(node.loc.selector, message: message) do |corrector|
135
+ autocorrect(corrector, node)
136
+ end
137
+ end
138
+
139
+ def attach_timezone_specifier?(date)
140
+ date.respond_to?(:value) && TIMEZONE_SPECIFIER.match?(date.value.to_s[-1])
132
141
  end
133
142
 
134
143
  def build_message(klass, method_name, node)
@@ -193,8 +202,9 @@ module RuboCop
193
202
 
194
203
  return if node.arguments?
195
204
 
196
- add_offense(selector_node,
197
- location: :selector, message: MSG_LOCALTIME)
205
+ add_offense(selector_node.loc.selector, message: MSG_LOCALTIME) do |corrector|
206
+ autocorrect(corrector, selector_node)
207
+ end
198
208
  end
199
209
 
200
210
  def not_danger_chain?(chain)
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop checks for the use of `Time.zone=` method.
7
+ #
8
+ # The `zone` attribute persists for the rest of the Ruby runtime, potentially causing
9
+ # unexpected behaviour at a later time.
10
+ # Using `Time.use_zone` ensures the code passed in block is the only place Time.zone is affected.
11
+ # It eliminates the possibility of a `zone` sticking around longer than intended.
12
+ #
13
+ # @example
14
+ # # bad
15
+ # Time.zone = 'EST'
16
+ #
17
+ # # good
18
+ # Time.use_zone('EST') do
19
+ # end
20
+ #
21
+ class TimeZoneAssignment < Base
22
+ MSG = 'Use `Time.use_zone` with blocks instead of `Time.zone=`.'
23
+ RESTRICT_ON_SEND = %i[zone=].freeze
24
+
25
+ def_node_matcher :time_zone_assignement?, <<~PATTERN
26
+ (send (const nil? :Time) :zone= ...)
27
+ PATTERN
28
+
29
+ def on_send(node)
30
+ return unless time_zone_assignement?(node)
31
+
32
+ add_offense(node)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -45,11 +45,13 @@ module RuboCop
45
45
  # # good
46
46
  # Model.distinct.pluck(:id)
47
47
  #
48
- class UniqBeforePluck < RuboCop::Cop::Cop
48
+ class UniqBeforePluck < Base
49
49
  include ConfigurableEnforcedStyle
50
50
  include RangeHelp
51
+ extend AutoCorrector
51
52
 
52
53
  MSG = 'Use `distinct` before `pluck`.'
54
+ RESTRICT_ON_SEND = %i[uniq distinct pluck].freeze
53
55
  NEWLINE = "\n"
54
56
  PATTERN = '[!^block (send (send %<type>s :pluck ...) ' \
55
57
  '${:uniq :distinct} ...)]'
@@ -69,11 +71,7 @@ module RuboCop
69
71
 
70
72
  return unless method
71
73
 
72
- add_offense(node, location: :selector)
73
- end
74
-
75
- def autocorrect(node)
76
- lambda do |corrector|
74
+ add_offense(node.loc.selector) do |corrector|
77
75
  method = node.method_name
78
76
 
79
77
  corrector.remove(dot_method_with_whitespace(method, node))
@@ -24,13 +24,13 @@ module RuboCop
24
24
  # # good - even if the schema does not have a unique index
25
25
  # validates :account, length: { minimum: MIN_LENGTH }
26
26
  #
27
- class UniqueValidationWithoutIndex < Cop
27
+ class UniqueValidationWithoutIndex < Base
28
28
  include ActiveRecordHelper
29
29
 
30
30
  MSG = 'Uniqueness validation should be with a unique index.'
31
+ RESTRICT_ON_SEND = %i[validates].freeze
31
32
 
32
33
  def on_send(node)
33
- return unless node.method?(:validates)
34
34
  return unless uniqueness_part(node)
35
35
  return if condition_part?(node)
36
36
  return unless schema
@@ -46,6 +46,8 @@ module RuboCop
46
46
 
47
47
  def find_schema_information(node)
48
48
  klass = class_node(node)
49
+ return unless klass
50
+
49
51
  table = schema.table_by(name: table_name(klass))
50
52
  names = column_names(node)
51
53
 
@@ -17,7 +17,7 @@ module RuboCop
17
17
  # # good
18
18
  # Rails.env.production?
19
19
  # Rails.env == 'production'
20
- class UnknownEnv < Cop
20
+ class UnknownEnv < Base
21
21
  MSG = 'Unknown environment `%<name>s`.'
22
22
  MSG_SIMILAR = 'Unknown environment `%<name>s`. ' \
23
23
  'Did you mean `%<similar>s`?'
@@ -41,7 +41,7 @@ module RuboCop
41
41
 
42
42
  def on_send(node)
43
43
  unknown_environment_predicate?(node) do |name|
44
- add_offense(node, location: :selector, message: message(name))
44
+ add_offense(node.loc.selector, message: message(name))
45
45
  end
46
46
 
47
47
  unknown_environment_equal?(node) do |str_node|
@@ -62,7 +62,7 @@ module RuboCop
62
62
  # DidYouMean::SpellChecker is not available in all versions of Ruby,
63
63
  # and even on versions where it *is* available (>= 2.3), it is not
64
64
  # always required correctly. So we do a feature check first. See:
65
- # https://github.com/rubocop-hq/rubocop/issues/7979
65
+ # https://github.com/rubocop/rubocop/issues/7979
66
66
  similar_names = if defined?(DidYouMean::SpellChecker)
67
67
  spell_checker = DidYouMean::SpellChecker.new(dictionary: environments)
68
68
  spell_checker.correct(name)
@@ -32,7 +32,9 @@ module RuboCop
32
32
  # validates :foo, size: true
33
33
  # validates :foo, uniqueness: true
34
34
  #
35
- class Validation < Cop
35
+ class Validation < Base
36
+ extend AutoCorrector
37
+
36
38
  MSG = 'Prefer the new style validations `%<prefer>s` over ' \
37
39
  '`%<current>s`.'
38
40
 
@@ -50,22 +52,20 @@ module RuboCop
50
52
  uniqueness
51
53
  ].freeze
52
54
 
53
- DENYLIST = TYPES.map { |p| "validates_#{p}_of".to_sym }.freeze
55
+ RESTRICT_ON_SEND = TYPES.map { |p| "validates_#{p}_of".to_sym }.freeze
54
56
  ALLOWLIST = TYPES.map { |p| "validates :column, #{p}: value" }.freeze
55
57
 
56
58
  def on_send(node)
57
- return unless !node.receiver && DENYLIST.include?(node.method_name)
59
+ return if node.receiver
58
60
 
59
- add_offense(node, location: :selector)
60
- end
61
+ range = node.loc.selector
61
62
 
62
- def autocorrect(node)
63
- last_argument = node.arguments.last
64
- return if !last_argument.literal? && !last_argument.splat_type? &&
65
- !frozen_array_argument?(last_argument)
63
+ add_offense(range, message: message(node)) do |corrector|
64
+ last_argument = node.arguments.last
65
+ return if !last_argument.literal? && !last_argument.splat_type? &&
66
+ !frozen_array_argument?(last_argument)
66
67
 
67
- lambda do |corrector|
68
- corrector.replace(node.loc.selector, 'validates')
68
+ corrector.replace(range, 'validates')
69
69
  correct_validate_type(corrector, node)
70
70
  end
71
71
  end
@@ -73,12 +73,13 @@ module RuboCop
73
73
  private
74
74
 
75
75
  def message(node)
76
- format(MSG, prefer: preferred_method(node.method_name),
77
- current: node.method_name)
76
+ method_name = node.method_name
77
+
78
+ format(MSG, prefer: preferred_method(method_name), current: method_name)
78
79
  end
79
80
 
80
81
  def preferred_method(method)
81
- ALLOWLIST[DENYLIST.index(method.to_sym)]
82
+ ALLOWLIST[RESTRICT_ON_SEND.index(method.to_sym)]
82
83
  end
83
84
 
84
85
  def correct_validate_type(corrector, node)
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop identifies places where manually constructed SQL
7
+ # in `where` can be replaced with `where(attribute: value)`.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # User.where('name = ?', 'Gabe')
12
+ # User.where('name = :name', name: 'Gabe')
13
+ # User.where('name IS NULL')
14
+ # User.where('name IN (?)', ['john', 'jane'])
15
+ # User.where('name IN (:names)', names: ['john', 'jane'])
16
+ # User.where('users.name = :name', name: 'Gabe')
17
+ #
18
+ # # good
19
+ # User.where(name: 'Gabe')
20
+ # User.where(name: nil)
21
+ # User.where(name: ['john', 'jane'])
22
+ # User.where(users: { name: 'Gabe' })
23
+ class WhereEquals < Base
24
+ include RangeHelp
25
+ extend AutoCorrector
26
+
27
+ MSG = 'Use `%<good_method>s` instead of manually constructing SQL.'
28
+ RESTRICT_ON_SEND = %i[where].freeze
29
+
30
+ def_node_matcher :where_method_call?, <<~PATTERN
31
+ {
32
+ (send _ :where (array $str_type? $_ ?))
33
+ (send _ :where $str_type? $_ ?)
34
+ }
35
+ PATTERN
36
+
37
+ def on_send(node)
38
+ where_method_call?(node) do |template_node, value_node|
39
+ value_node = value_node.first
40
+
41
+ range = offense_range(node)
42
+
43
+ column_and_value = extract_column_and_value(template_node, value_node)
44
+ return unless column_and_value
45
+
46
+ good_method = build_good_method(*column_and_value)
47
+ message = format(MSG, good_method: good_method)
48
+
49
+ add_offense(range, message: message) do |corrector|
50
+ corrector.replace(range, good_method)
51
+ end
52
+ end
53
+ end
54
+
55
+ EQ_ANONYMOUS_RE = /\A([\w.]+)\s+=\s+\?\z/.freeze # column = ?
56
+ IN_ANONYMOUS_RE = /\A([\w.]+)\s+IN\s+\(\?\)\z/i.freeze # column IN (?)
57
+ EQ_NAMED_RE = /\A([\w.]+)\s+=\s+:(\w+)\z/.freeze # column = :column
58
+ IN_NAMED_RE = /\A([\w.]+)\s+IN\s+\(:(\w+)\)\z/i.freeze # column IN (:column)
59
+ IS_NULL_RE = /\A([\w.]+)\s+IS\s+NULL\z/i.freeze # column IS NULL
60
+
61
+ private
62
+
63
+ def offense_range(node)
64
+ range_between(node.loc.selector.begin_pos, node.loc.expression.end_pos)
65
+ end
66
+
67
+ def extract_column_and_value(template_node, value_node)
68
+ value =
69
+ case template_node.value
70
+ when EQ_ANONYMOUS_RE, IN_ANONYMOUS_RE
71
+ value_node.source
72
+ when EQ_NAMED_RE, IN_NAMED_RE
73
+ return unless value_node&.hash_type?
74
+
75
+ pair = value_node.pairs.find { |p| p.key.value.to_sym == Regexp.last_match(2).to_sym }
76
+ pair.value.source
77
+ when IS_NULL_RE
78
+ 'nil'
79
+ else
80
+ return
81
+ end
82
+
83
+ [Regexp.last_match(1), value]
84
+ end
85
+
86
+ def build_good_method(column, value)
87
+ if column.include?('.')
88
+ table, column = column.split('.')
89
+
90
+ "where(#{table}: { #{column}: #{value} })"
91
+ else
92
+ "where(#{column}: #{value})"
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end