rubocop-rails 2.8.0 → 2.10.1

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 (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