rubocop-rails 2.6.0 → 2.9.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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -0
  3. data/config/default.yml +189 -6
  4. data/lib/rubocop/cop/mixin/active_record_helper.rb +12 -3
  5. data/lib/rubocop/cop/mixin/enforce_superclass.rb +40 -0
  6. data/lib/rubocop/cop/mixin/index_method.rb +25 -11
  7. data/lib/rubocop/cop/rails/action_filter.rb +10 -14
  8. data/lib/rubocop/cop/rails/active_record_aliases.rb +13 -17
  9. data/lib/rubocop/cop/rails/active_record_callbacks_order.rb +148 -0
  10. data/lib/rubocop/cop/rails/active_record_override.rb +1 -1
  11. data/lib/rubocop/cop/rails/active_support_aliases.rb +12 -21
  12. data/lib/rubocop/cop/rails/after_commit_override.rb +91 -0
  13. data/lib/rubocop/cop/rails/application_controller.rb +3 -7
  14. data/lib/rubocop/cop/rails/application_job.rb +2 -1
  15. data/lib/rubocop/cop/rails/application_mailer.rb +2 -7
  16. data/lib/rubocop/cop/rails/application_record.rb +2 -7
  17. data/lib/rubocop/cop/rails/arel_star.rb +41 -0
  18. data/lib/rubocop/cop/rails/assert_not.rb +8 -10
  19. data/lib/rubocop/cop/rails/attribute_default_block_value.rb +90 -0
  20. data/lib/rubocop/cop/rails/belongs_to.rb +9 -18
  21. data/lib/rubocop/cop/rails/blank.rb +27 -27
  22. data/lib/rubocop/cop/rails/bulk_change_table.rb +1 -1
  23. data/lib/rubocop/cop/rails/content_tag.rb +20 -33
  24. data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +2 -1
  25. data/lib/rubocop/cop/rails/date.rb +10 -11
  26. data/lib/rubocop/cop/rails/default_scope.rb +61 -0
  27. data/lib/rubocop/cop/rails/delegate.rb +10 -10
  28. data/lib/rubocop/cop/rails/delegate_allow_blank.rb +7 -8
  29. data/lib/rubocop/cop/rails/dynamic_find_by.rb +13 -11
  30. data/lib/rubocop/cop/rails/enum_hash.rb +11 -10
  31. data/lib/rubocop/cop/rails/enum_uniqueness.rb +2 -1
  32. data/lib/rubocop/cop/rails/environment_comparison.rb +18 -14
  33. data/lib/rubocop/cop/rails/exit.rb +4 -10
  34. data/lib/rubocop/cop/rails/file_path.rb +5 -4
  35. data/lib/rubocop/cop/rails/find_by.rb +13 -13
  36. data/lib/rubocop/cop/rails/find_by_id.rb +94 -0
  37. data/lib/rubocop/cop/rails/find_each.rb +16 -14
  38. data/lib/rubocop/cop/rails/has_and_belongs_to_many.rb +3 -2
  39. data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +4 -7
  40. data/lib/rubocop/cop/rails/helper_instance_variable.rb +4 -2
  41. data/lib/rubocop/cop/rails/http_positional_arguments.rb +25 -21
  42. data/lib/rubocop/cop/rails/http_status.rb +7 -9
  43. data/lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb +8 -6
  44. data/lib/rubocop/cop/rails/index_by.rb +11 -2
  45. data/lib/rubocop/cop/rails/index_with.rb +11 -2
  46. data/lib/rubocop/cop/rails/inquiry.rb +39 -0
  47. data/lib/rubocop/cop/rails/inverse_of.rb +3 -2
  48. data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +17 -15
  49. data/lib/rubocop/cop/rails/link_to_blank.rb +20 -20
  50. data/lib/rubocop/cop/rails/mailer_name.rb +86 -0
  51. data/lib/rubocop/cop/rails/match_route.rb +120 -0
  52. data/lib/rubocop/cop/rails/negate_include.rb +41 -0
  53. data/lib/rubocop/cop/rails/not_null_column.rb +2 -1
  54. data/lib/rubocop/cop/rails/order_by_id.rb +52 -0
  55. data/lib/rubocop/cop/rails/output.rb +5 -2
  56. data/lib/rubocop/cop/rails/output_safety.rb +3 -2
  57. data/lib/rubocop/cop/rails/pick.rb +21 -15
  58. data/lib/rubocop/cop/rails/pluck.rb +56 -0
  59. data/lib/rubocop/cop/rails/pluck_id.rb +56 -0
  60. data/lib/rubocop/cop/rails/pluck_in_where.rb +70 -0
  61. data/lib/rubocop/cop/rails/pluralization_grammar.rb +10 -14
  62. data/lib/rubocop/cop/rails/presence.rb +12 -13
  63. data/lib/rubocop/cop/rails/present.rb +30 -24
  64. data/lib/rubocop/cop/rails/rake_environment.rb +9 -11
  65. data/lib/rubocop/cop/rails/read_write_attribute.rb +12 -11
  66. data/lib/rubocop/cop/rails/redundant_allow_nil.rb +29 -31
  67. data/lib/rubocop/cop/rails/redundant_foreign_key.rb +9 -12
  68. data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +11 -10
  69. data/lib/rubocop/cop/rails/reflection_class_name.rb +4 -3
  70. data/lib/rubocop/cop/rails/refute_methods.rb +9 -10
  71. data/lib/rubocop/cop/rails/relative_date_constant.rb +20 -9
  72. data/lib/rubocop/cop/rails/render_inline.rb +41 -0
  73. data/lib/rubocop/cop/rails/render_plain_text.rb +71 -0
  74. data/lib/rubocop/cop/rails/request_referer.rb +7 -7
  75. data/lib/rubocop/cop/rails/reversible_migration.rb +82 -7
  76. data/lib/rubocop/cop/rails/safe_navigation.rb +12 -11
  77. data/lib/rubocop/cop/rails/safe_navigation_with_blank.rb +5 -10
  78. data/lib/rubocop/cop/rails/save_bang.rb +19 -22
  79. data/lib/rubocop/cop/rails/scope_args.rb +2 -1
  80. data/lib/rubocop/cop/rails/short_i18n.rb +74 -0
  81. data/lib/rubocop/cop/rails/skips_model_validations.rb +46 -11
  82. data/lib/rubocop/cop/rails/squished_sql_heredocs.rb +82 -0
  83. data/lib/rubocop/cop/rails/time_zone.rb +22 -20
  84. data/lib/rubocop/cop/rails/uniq_before_pluck.rb +10 -10
  85. data/lib/rubocop/cop/rails/unique_validation_without_index.rb +18 -8
  86. data/lib/rubocop/cop/rails/unknown_env.rb +15 -4
  87. data/lib/rubocop/cop/rails/validation.rb +15 -14
  88. data/lib/rubocop/cop/rails/where_equals.rb +94 -0
  89. data/lib/rubocop/cop/rails/where_exists.rb +126 -0
  90. data/lib/rubocop/cop/rails/where_not.rb +97 -0
  91. data/lib/rubocop/cop/rails_cops.rb +22 -0
  92. data/lib/rubocop/rails/schema_loader.rb +4 -4
  93. data/lib/rubocop/rails/schema_loader/schema.rb +5 -5
  94. data/lib/rubocop/rails/version.rb +5 -1
  95. metadata +37 -9
@@ -43,8 +43,9 @@ module RuboCop
43
43
  # # good
44
44
  # Time.current
45
45
  # Time.at(timestamp).in_time_zone
46
- class TimeZone < Cop
46
+ class TimeZone < Base
47
47
  include ConfigurableEnforcedStyle
48
+ extend AutoCorrector
48
49
 
49
50
  MSG = 'Do not use `%<current>s` without zone. Use `%<prefer>s` ' \
50
51
  'instead.'
@@ -71,26 +72,24 @@ module RuboCop
71
72
  check_time_node(klass, node.parent) if klass == :Time
72
73
  end
73
74
 
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')
75
+ private
78
76
 
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
77
+ def autocorrect(corrector, node)
78
+ # add `.zone`: `Time.at` => `Time.zone.at`
79
+ corrector.insert_after(node.children[0].source_range, '.zone')
86
80
 
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)
81
+ case node.method_name
82
+ when :current
83
+ # replace `Time.zone.current` => `Time.zone.now`
84
+ corrector.replace(node.loc.selector, 'now')
85
+ when :new
86
+ autocorrect_time_new(node, corrector)
90
87
  end
91
- end
92
88
 
93
- private
89
+ # prefer `Time` over `DateTime` class
90
+ corrector.replace(node.children.first.source_range, 'Time') if strict?
91
+ remove_redundant_in_time_zone(corrector, node)
92
+ end
94
93
 
95
94
  def autocorrect_time_new(node, corrector)
96
95
  if node.arguments?
@@ -128,7 +127,9 @@ module RuboCop
128
127
 
129
128
  message = build_message(klass, method_name, node)
130
129
 
131
- add_offense(node, location: :selector, message: message)
130
+ add_offense(node.loc.selector, message: message) do |corrector|
131
+ autocorrect(corrector, node)
132
+ end
132
133
  end
133
134
 
134
135
  def build_message(klass, method_name, node)
@@ -193,8 +194,9 @@ module RuboCop
193
194
 
194
195
  return if node.arguments?
195
196
 
196
- add_offense(selector_node,
197
- location: :selector, message: MSG_LOCALTIME)
197
+ add_offense(selector_node.loc.selector, message: MSG_LOCALTIME) do |corrector|
198
+ autocorrect(corrector, selector_node)
199
+ end
198
200
  end
199
201
 
200
202
  def not_danger_chain?(chain)
@@ -18,12 +18,14 @@ module RuboCop
18
18
  # ActiveRecord::Relation vs a call to pluck on an
19
19
  # ActiveRecord::Associations::CollectionProxy.
20
20
  #
21
+ # This cop is unsafe because the behavior may change depending on the
22
+ # database collation.
21
23
  # Autocorrect is disabled by default for this cop since it may generate
22
24
  # false positives.
23
25
  #
24
26
  # @example EnforcedStyle: conservative (default)
25
27
  # # bad
26
- # Model.pluck(:id).distinct
28
+ # Model.pluck(:id).uniq
27
29
  #
28
30
  # # good
29
31
  # Model.distinct.pluck(:id)
@@ -31,23 +33,25 @@ module RuboCop
31
33
  # @example EnforcedStyle: aggressive
32
34
  # # bad
33
35
  # # this will return a Relation that pluck is called on
34
- # Model.where(cond: true).pluck(:id).distinct
36
+ # Model.where(cond: true).pluck(:id).uniq
35
37
  #
36
38
  # # bad
37
39
  # # an association on an instance will return a CollectionProxy
38
- # instance.assoc.pluck(:id).distinct
40
+ # instance.assoc.pluck(:id).uniq
39
41
  #
40
42
  # # bad
41
- # Model.pluck(:id).distinct
43
+ # Model.pluck(:id).uniq
42
44
  #
43
45
  # # good
44
46
  # Model.distinct.pluck(:id)
45
47
  #
46
- class UniqBeforePluck < RuboCop::Cop::Cop
48
+ class UniqBeforePluck < Base
47
49
  include ConfigurableEnforcedStyle
48
50
  include RangeHelp
51
+ extend AutoCorrector
49
52
 
50
53
  MSG = 'Use `distinct` before `pluck`.'
54
+ RESTRICT_ON_SEND = %i[uniq distinct pluck].freeze
51
55
  NEWLINE = "\n"
52
56
  PATTERN = '[!^block (send (send %<type>s :pluck ...) ' \
53
57
  '${:uniq :distinct} ...)]'
@@ -67,11 +71,7 @@ module RuboCop
67
71
 
68
72
  return unless method
69
73
 
70
- add_offense(node, location: :selector)
71
- end
72
-
73
- def autocorrect(node)
74
- lambda do |corrector|
74
+ add_offense(node.loc.selector) do |corrector|
75
75
  method = node.method_name
76
76
 
77
77
  corrector.remove(dot_method_with_whitespace(method, node))
@@ -24,33 +24,37 @@ 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
37
- return if with_index?(node)
37
+
38
+ klass, table, names = find_schema_information(node)
39
+ return unless names
40
+ return if with_index?(klass, table, names)
38
41
 
39
42
  add_offense(node)
40
43
  end
41
44
 
42
45
  private
43
46
 
44
- def with_index?(node)
47
+ def find_schema_information(node)
45
48
  klass = class_node(node)
46
- return true unless klass # Skip analysis
49
+ return unless klass
47
50
 
48
51
  table = schema.table_by(name: table_name(klass))
49
- return true unless table # Skip analysis if it can't find the table
50
-
51
52
  names = column_names(node)
52
- return true unless names
53
53
 
54
+ [klass, table, names]
55
+ end
56
+
57
+ def with_index?(klass, table, names)
54
58
  # Compatibility for Rails 4.2.
55
59
  add_indicies = schema.add_indicies_by(table_name: table_name(klass))
56
60
 
@@ -95,6 +99,8 @@ module RuboCop
95
99
  scope = find_scope(uniq)
96
100
  return unless scope
97
101
 
102
+ scope = unfreeze_scope(scope)
103
+
98
104
  case scope.type
99
105
  when :sym, :str
100
106
  [scope.value]
@@ -112,6 +118,10 @@ module RuboCop
112
118
  end
113
119
  end
114
120
 
121
+ def unfreeze_scope(scope)
122
+ scope.send_type? && scope.method?(:freeze) ? scope.children.first : scope
123
+ end
124
+
115
125
  def class_node(node)
116
126
  node.each_ancestor.find(&:class_type?)
117
127
  end
@@ -5,6 +5,9 @@ module RuboCop
5
5
  module Rails
6
6
  # This cop checks that environments called with `Rails.env` predicates
7
7
  # exist.
8
+ # By default the cop allows three environments which Rails ships with:
9
+ # `development`, `test`, and `production`.
10
+ # More can be added to the `Environments` config parameter.
8
11
  #
9
12
  # @example
10
13
  # # bad
@@ -14,7 +17,7 @@ module RuboCop
14
17
  # # good
15
18
  # Rails.env.production?
16
19
  # Rails.env == 'production'
17
- class UnknownEnv < Cop
20
+ class UnknownEnv < Base
18
21
  MSG = 'Unknown environment `%<name>s`.'
19
22
  MSG_SIMILAR = 'Unknown environment `%<name>s`. ' \
20
23
  'Did you mean `%<similar>s`?'
@@ -38,7 +41,7 @@ module RuboCop
38
41
 
39
42
  def on_send(node)
40
43
  unknown_environment_predicate?(node) do |name|
41
- add_offense(node, location: :selector, message: message(name))
44
+ add_offense(node.loc.selector, message: message(name))
42
45
  end
43
46
 
44
47
  unknown_environment_equal?(node) do |str_node|
@@ -56,8 +59,16 @@ module RuboCop
56
59
  def message(name)
57
60
  name = name.to_s.chomp('?')
58
61
 
59
- spell_checker = DidYouMean::SpellChecker.new(dictionary: environments)
60
- similar_names = spell_checker.correct(name)
62
+ # DidYouMean::SpellChecker is not available in all versions of Ruby,
63
+ # and even on versions where it *is* available (>= 2.3), it is not
64
+ # always required correctly. So we do a feature check first. See:
65
+ # https://github.com/rubocop-hq/rubocop/issues/7979
66
+ similar_names = if defined?(DidYouMean::SpellChecker)
67
+ spell_checker = DidYouMean::SpellChecker.new(dictionary: environments)
68
+ spell_checker.correct(name)
69
+ else
70
+ []
71
+ end
61
72
 
62
73
  if similar_names.empty?
63
74
  format(MSG, name: 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,94 @@
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
+ #
17
+ # # good
18
+ # User.where(name: 'Gabe')
19
+ # User.where(name: nil)
20
+ # User.where(name: ['john', 'jane'])
21
+ class WhereEquals < Base
22
+ include RangeHelp
23
+ extend AutoCorrector
24
+
25
+ MSG = 'Use `%<good_method>s` instead of manually constructing SQL.'
26
+ RESTRICT_ON_SEND = %i[where].freeze
27
+
28
+ def_node_matcher :where_method_call?, <<~PATTERN
29
+ {
30
+ (send _ :where (array $str_type? $_ ?))
31
+ (send _ :where $str_type? $_ ?)
32
+ }
33
+ PATTERN
34
+
35
+ def on_send(node)
36
+ where_method_call?(node) do |template_node, value_node|
37
+ value_node = value_node.first
38
+
39
+ range = offense_range(node)
40
+
41
+ column_and_value = extract_column_and_value(template_node, value_node)
42
+ return unless column_and_value
43
+
44
+ good_method = build_good_method(*column_and_value)
45
+ message = format(MSG, good_method: good_method)
46
+
47
+ add_offense(range, message: message) do |corrector|
48
+ corrector.replace(range, good_method)
49
+ end
50
+ end
51
+ end
52
+
53
+ EQ_ANONYMOUS_RE = /\A([\w.]+)\s+=\s+\?\z/.freeze # column = ?
54
+ IN_ANONYMOUS_RE = /\A([\w.]+)\s+IN\s+\(\?\)\z/i.freeze # column IN (?)
55
+ EQ_NAMED_RE = /\A([\w.]+)\s+=\s+:(\w+)\z/.freeze # column = :column
56
+ IN_NAMED_RE = /\A([\w.]+)\s+IN\s+\(:(\w+)\)\z/i.freeze # column IN (:column)
57
+ IS_NULL_RE = /\A([\w.]+)\s+IS\s+NULL\z/i.freeze # column IS NULL
58
+
59
+ private
60
+
61
+ def offense_range(node)
62
+ range_between(node.loc.selector.begin_pos, node.loc.expression.end_pos)
63
+ end
64
+
65
+ def extract_column_and_value(template_node, value_node)
66
+ value =
67
+ case template_node.value
68
+ when EQ_ANONYMOUS_RE, IN_ANONYMOUS_RE
69
+ value_node.source
70
+ when EQ_NAMED_RE, IN_NAMED_RE
71
+ return unless value_node.hash_type?
72
+
73
+ pair = value_node.pairs.find { |p| p.key.value.to_sym == Regexp.last_match(2).to_sym }
74
+ pair.value.source
75
+ when IS_NULL_RE
76
+ 'nil'
77
+ else
78
+ return
79
+ end
80
+
81
+ [Regexp.last_match(1), value]
82
+ end
83
+
84
+ def build_good_method(column, value)
85
+ if column.include?('.')
86
+ "where('#{column}' => #{value})"
87
+ else
88
+ "where(#{column}: #{value})"
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop enforces consistent style when using `exists?`.
7
+ #
8
+ # Two styles are supported for this cop. When EnforcedStyle is 'exists'
9
+ # then the cop enforces `exists?(...)` over `where(...).exists?`.
10
+ #
11
+ # When EnforcedStyle is 'where' then the cop enforces
12
+ # `where(...).exists?` over `exists?(...)`.
13
+ #
14
+ # @example EnforcedStyle: exists (default)
15
+ # # bad
16
+ # User.where(name: 'john').exists?
17
+ # User.where(['name = ?', 'john']).exists?
18
+ # User.where('name = ?', 'john').exists?
19
+ # user.posts.where(published: true).exists?
20
+ #
21
+ # # good
22
+ # User.exists?(name: 'john')
23
+ # User.where('length(name) > 10').exists?
24
+ # user.posts.exists?(published: true)
25
+ #
26
+ # @example EnforcedStyle: where
27
+ # # bad
28
+ # User.exists?(name: 'john')
29
+ # User.exists?(['name = ?', 'john'])
30
+ # User.exists?('name = ?', 'john')
31
+ # user.posts.exists?(published: true)
32
+ #
33
+ # # good
34
+ # User.where(name: 'john').exists?
35
+ # User.where(['name = ?', 'john']).exists?
36
+ # User.where('name = ?', 'john').exists?
37
+ # user.posts.where(published: true).exists?
38
+ # User.where('length(name) > 10').exists?
39
+ class WhereExists < Base
40
+ include ConfigurableEnforcedStyle
41
+ extend AutoCorrector
42
+
43
+ MSG = 'Prefer `%<good_method>s` over `%<bad_method>s`.'
44
+ RESTRICT_ON_SEND = %i[exists?].freeze
45
+
46
+ def_node_matcher :where_exists_call?, <<~PATTERN
47
+ (send (send _ :where $...) :exists?)
48
+ PATTERN
49
+
50
+ def_node_matcher :exists_with_args?, <<~PATTERN
51
+ (send _ :exists? $...)
52
+ PATTERN
53
+
54
+ def on_send(node)
55
+ find_offenses(node) do |args|
56
+ return unless convertable_args?(args)
57
+
58
+ range = correction_range(node)
59
+ good_method = build_good_method(args)
60
+ message = format(MSG, good_method: good_method, bad_method: range.source)
61
+
62
+ add_offense(range, message: message) do |corrector|
63
+ corrector.replace(range, good_method)
64
+ end
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def where_style?
71
+ style == :where
72
+ end
73
+
74
+ def exists_style?
75
+ style == :exists
76
+ end
77
+
78
+ def find_offenses(node, &block)
79
+ if exists_style?
80
+ where_exists_call?(node, &block)
81
+ elsif where_style?
82
+ exists_with_args?(node, &block)
83
+ end
84
+ end
85
+
86
+ def convertable_args?(args)
87
+ return false if args.empty?
88
+
89
+ args.size > 1 || args[0].hash_type? || args[0].array_type?
90
+ end
91
+
92
+ def correction_range(node)
93
+ if exists_style?
94
+ node.receiver.loc.selector.join(node.loc.selector)
95
+ elsif where_style?
96
+ node.loc.selector.with(end_pos: node.loc.expression.end_pos)
97
+ end
98
+ end
99
+
100
+ def build_good_method(args)
101
+ if exists_style?
102
+ build_good_method_exists(args)
103
+ elsif where_style?
104
+ build_good_method_where(args)
105
+ end
106
+ end
107
+
108
+ def build_good_method_exists(args)
109
+ if args.size > 1
110
+ "exists?([#{args.map(&:source).join(', ')}])"
111
+ else
112
+ "exists?(#{args[0].source})"
113
+ end
114
+ end
115
+
116
+ def build_good_method_where(args)
117
+ if args.size > 1
118
+ "where(#{args.map(&:source).join(', ')}).exists?"
119
+ else
120
+ "where(#{args[0].source}).exists?"
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end