rubocop-rails 2.6.0 → 2.9.0

Sign up to get free protection for your applications and to get access to all the features.
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