rubocop-rails 2.7.1 → 2.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8cffb4c138d270540b5821e23b2c5450a1939d4b01a2720ce400a8bb510ff90c
4
- data.tar.gz: e0702c6ae4a7a43a01034b32d64ad67bac55659d6a5ec471685bda3c6d2a43fe
3
+ metadata.gz: 6e89f00783e4dce6642e43ba4a4bf8ee29d078131f71404230db8fd2a1f87248
4
+ data.tar.gz: bfbca38eb400e89e260b572babc327afec01b25bad89178179e8276ef7ce651c
5
5
  SHA512:
6
- metadata.gz: 0ce7f6953d24239b8ce71a2e759da5007eb94a752f34ccb8d738273f5f75c63f57664042964f9766652c16e076996bb294edafc7d687a923c60ae2f56328afd3
7
- data.tar.gz: b810b20d35548b34813946fff138c204fe0ce4f64a68b2f09e63a69c102ad44cf476a543b8bcf87f9caf42d5312ec277e14d23650100df21fb27fb14cc246216
6
+ metadata.gz: 241d43809d5cbfa4684fa83c1b92c9069447bae05ca0235d137e6934a2a9ad35ff3e00fba46928e793045ee1cf35dc3a520cca9f177e8c3280530282eb52ac06
7
+ data.tar.gz: 7019b400331651ad86a94e9562f93e31c32025ae5ed9c8c5da0f62c65974b21052438908a535da9314a6b903ea3ff6815dc7a0a6350ce3db9d6ab1d0ac18d015
@@ -62,6 +62,14 @@ Rails/ActiveSupportAliases:
62
62
  Enabled: true
63
63
  VersionAdded: '0.48'
64
64
 
65
+ Rails/AfterCommitOverride:
66
+ Description: >-
67
+ This cop enforces that there is only one call to `after_commit`
68
+ (and its aliases - `after_create_commit`, `after_update_commit`,
69
+ and `after_destroy_commit`) with the same callback name per model.
70
+ Enabled: 'pending'
71
+ VersionAdded: '2.8'
72
+
65
73
  Rails/ApplicationController:
66
74
  Description: 'Check that controllers subclass ApplicationController.'
67
75
  Enabled: true
@@ -313,14 +321,16 @@ Rails/IgnoredSkipActionFilterOption:
313
321
  - app/controllers/**/*.rb
314
322
 
315
323
  Rails/IndexBy:
316
- Description: 'Prefer `index_by` over `each_with_object` or `map`.'
324
+ Description: 'Prefer `index_by` over `each_with_object`, `to_h`, or `map`.'
317
325
  Enabled: true
318
326
  VersionAdded: '2.5'
327
+ VersionChanged: '2.8'
319
328
 
320
329
  Rails/IndexWith:
321
- Description: 'Prefer `index_with` over `each_with_object` or `map`.'
330
+ Description: 'Prefer `index_with` over `each_with_object`, `to_h`, or `map`.'
322
331
  Enabled: true
323
332
  VersionAdded: '2.5'
333
+ VersionChanged: '2.8'
324
334
 
325
335
  Rails/Inquiry:
326
336
  Description: "Prefer Ruby's comparison operators over Active Support's `Array#inquiry` and `String#inquiry`."
@@ -357,6 +367,7 @@ Rails/MailerName:
357
367
  Description: 'Mailer should end with `Mailer` suffix.'
358
368
  StyleGuide: 'https://rails.rubystyle.guide/#mailer-name'
359
369
  Enabled: 'pending'
370
+ SafeAutoCorrect: false
360
371
  VersionAdded: '2.7'
361
372
  Include:
362
373
  - app/mailers/**/*.rb
@@ -385,6 +396,14 @@ Rails/NotNullColumn:
385
396
  Include:
386
397
  - db/migrate/*.rb
387
398
 
399
+ Rails/OrderById:
400
+ Description: >-
401
+ Do not use the `id` column for ordering.
402
+ Use a timestamp column to order chronologically.
403
+ StyleGuide: 'https://rails.rubystyle.guide/#order-by-id'
404
+ Enabled: false
405
+ VersionAdded: '2.8'
406
+
388
407
  Rails/Output:
389
408
  Description: 'Checks for calls to puts, print, etc.'
390
409
  Enabled: true
@@ -426,6 +445,11 @@ Rails/PluckInWhere:
426
445
  Enabled: 'pending'
427
446
  Safe: false
428
447
  VersionAdded: '2.7'
448
+ VersionChanged: '2.8'
449
+ EnforcedStyle: conservative
450
+ SupportedStyles:
451
+ - conservative
452
+ - aggressive
429
453
 
430
454
  Rails/PluralizationGrammar:
431
455
  Description: 'Checks for incorrect grammar when using methods like `3.day.ago`.'
@@ -624,6 +648,12 @@ Rails/SkipsModelValidations:
624
648
  - upsert_all
625
649
  AllowedMethods: []
626
650
 
651
+ Rails/SquishedSQLHeredocs:
652
+ Description: 'Checks SQL heredocs to use `.squish`.'
653
+ StyleGuide: 'https://rails.rubystyle.guide/#squished-heredocs'
654
+ Enabled: 'pending'
655
+ VersionAdded: '2.8'
656
+
627
657
  Rails/TimeZone:
628
658
  Description: 'Checks the correct usage of time zone aware methods.'
629
659
  StyleGuide: 'https://rails.rubystyle.guide#time'
@@ -643,11 +673,12 @@ Rails/UniqBeforePluck:
643
673
  Description: 'Prefer the use of uniq or distinct before pluck.'
644
674
  Enabled: true
645
675
  VersionAdded: '0.40'
646
- VersionChanged: '2.6'
676
+ VersionChanged: '2.8'
647
677
  EnforcedStyle: conservative
648
678
  SupportedStyles:
649
679
  - conservative
650
680
  - aggressive
681
+ SafeAutoCorrect: false
651
682
  AutoCorrect: false
652
683
 
653
684
  Rails/UniqueValidationWithoutIndex:
@@ -677,7 +708,18 @@ Rails/Validation:
677
708
  Rails/WhereExists:
678
709
  Description: 'Prefer `exists?(...)` over `where(...).exists?`.'
679
710
  Enabled: 'pending'
711
+ EnforcedStyle: exists
712
+ SupportedStyles:
713
+ - exists
714
+ - where
680
715
  VersionAdded: '2.7'
716
+ VersionChanged: '2.8'
717
+
718
+ Rails/WhereNot:
719
+ Description: 'Use `where.not(...)` instead of manually constructing negated SQL in `where`.'
720
+ StyleGuide: 'https://rails.rubystyle.guide/#where-not'
721
+ Enabled: 'pending'
722
+ VersionAdded: '2.8'
681
723
 
682
724
  # Accept `redirect_to(...) and return` and similar cases.
683
725
  Style/AndOr:
@@ -8,6 +8,12 @@ module RuboCop
8
8
  on_bad_each_with_object(node) do |*match|
9
9
  handle_possible_offense(node, match, 'each_with_object')
10
10
  end
11
+
12
+ return if target_ruby_version < 2.6
13
+
14
+ on_bad_to_h(node) do |*match|
15
+ handle_possible_offense(node, match, 'to_h { ... }')
16
+ end
11
17
  end
12
18
 
13
19
  def on_send(node)
@@ -40,6 +46,11 @@ module RuboCop
40
46
  raise NotImplementedError
41
47
  end
42
48
 
49
+ # @abstract Implemented with `def_node_matcher`
50
+ def on_bad_to_h(_node)
51
+ raise NotImplementedError
52
+ end
53
+
43
54
  # @abstract Implemented with `def_node_matcher`
44
55
  def on_bad_map_to_h(_node)
45
56
  raise NotImplementedError
@@ -73,6 +84,8 @@ module RuboCop
73
84
  def prepare_correction(node)
74
85
  if (match = on_bad_each_with_object(node))
75
86
  Autocorrection.from_each_with_object(node, match)
87
+ elsif (match = on_bad_to_h(node))
88
+ Autocorrection.from_to_h(node, match)
76
89
  elsif (match = on_bad_map_to_h(node))
77
90
  Autocorrection.from_map_to_h(node, match)
78
91
  elsif (match = on_bad_hash_brackets_map(node))
@@ -111,6 +124,10 @@ module RuboCop
111
124
  new(match, node, 0, 0)
112
125
  end
113
126
 
127
+ def self.from_to_h(node, match)
128
+ new(match, node, 0, 0)
129
+ end
130
+
114
131
  def self.from_map_to_h(node, match)
115
132
  strip_trailing_chars = 0
116
133
 
@@ -44,9 +44,7 @@ module RuboCop
44
44
  after_touch
45
45
  ].freeze
46
46
 
47
- CALLBACKS_ORDER_MAP = Hash[
48
- CALLBACKS_IN_ORDER.map.with_index { |name, index| [name, index] }
49
- ].freeze
47
+ CALLBACKS_ORDER_MAP = CALLBACKS_IN_ORDER.each_with_index.to_h.freeze
50
48
 
51
49
  def on_class(class_node)
52
50
  previous_index = -1
@@ -68,7 +66,7 @@ module RuboCop
68
66
 
69
67
  # Autocorrect by swapping between two nodes autocorrecting them
70
68
  def autocorrect(node)
71
- previous = left_siblings_of(node).find do |sibling|
69
+ previous = left_siblings_of(node).reverse_each.find do |sibling|
72
70
  callback?(sibling)
73
71
  end
74
72
 
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop enforces that there is only one call to `after_commit`
7
+ # (and its aliases - `after_create_commit`, `after_update_commit`,
8
+ # and `after_destroy_commit`) with the same callback name per model.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # # This won't be triggered.
13
+ # after_create_commit :log_action
14
+ #
15
+ # # This will override the callback added by
16
+ # # after_create_commit.
17
+ # after_update_commit :log_action
18
+ #
19
+ # # bad
20
+ # # This won't be triggered.
21
+ # after_commit :log_action, on: :create
22
+ # # This won't be triggered.
23
+ # after_update_commit :log_action
24
+ # # This will override both previous callbacks.
25
+ # after_commit :log_action, on: :destroy
26
+ #
27
+ # # good
28
+ # after_save_commit :log_action
29
+ #
30
+ # # good
31
+ # after_create_commit :log_create_action
32
+ # after_update_commit :log_update_action
33
+ #
34
+ class AfterCommitOverride < Cop
35
+ MSG = 'There can only be one `after_*_commit :%<name>s` hook defined for a model.'
36
+
37
+ AFTER_COMMIT_CALLBACKS = %i[
38
+ after_commit
39
+ after_create_commit
40
+ after_update_commit
41
+ after_save_commit
42
+ after_destroy_commit
43
+ ].freeze
44
+
45
+ def on_class(class_node)
46
+ seen_callback_names = {}
47
+
48
+ each_after_commit_callback(class_node) do |node|
49
+ callback_name = node.arguments[0].value
50
+ if seen_callback_names.key?(callback_name)
51
+ add_offense(node, message: format(MSG, name: callback_name))
52
+ else
53
+ seen_callback_names[callback_name] = true
54
+ end
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def each_after_commit_callback(class_node)
61
+ class_send_nodes(class_node).each do |node|
62
+ yield node if after_commit_callback?(node)
63
+ end
64
+ end
65
+
66
+ def class_send_nodes(class_node)
67
+ class_def = class_node.body
68
+
69
+ return [] unless class_def
70
+
71
+ if class_def.send_type?
72
+ [class_def]
73
+ else
74
+ class_def.each_child_node(:send).to_a
75
+ end
76
+ end
77
+
78
+ def after_commit_callback?(node)
79
+ AFTER_COMMIT_CALLBACKS.include?(node.method_name)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -43,7 +43,7 @@ module RuboCop
43
43
  range = correction_range(node)
44
44
 
45
45
  rest_args = node.arguments.drop(1)
46
- replacement = "tag.#{node.first_argument.value}(#{rest_args.map(&:source).join(', ')})"
46
+ replacement = "tag.#{node.first_argument.value.to_s.underscore}(#{rest_args.map(&:source).join(', ')})"
47
47
 
48
48
  corrector.replace(range, replacement)
49
49
  else
@@ -57,7 +57,7 @@ module RuboCop
57
57
  def method_name?(node)
58
58
  return false unless node.str_type? || node.sym_type?
59
59
 
60
- /^[a-zA-Z_][a-zA-Z_0-9]*$/.match?(node.value)
60
+ /^[a-zA-Z_][a-zA-Z_\-0-9]*$/.match?(node.value)
61
61
  end
62
62
 
63
63
  def correction_range(node)
@@ -90,7 +90,7 @@ module RuboCop
90
90
  end
91
91
 
92
92
  def string_with_slash?(node)
93
- node.str_type? && node.source.match?(%r{/})
93
+ node.str_type? && node.source.include?('/')
94
94
  end
95
95
 
96
96
  def register_offense(node)
@@ -52,11 +52,7 @@ module RuboCop
52
52
 
53
53
  def on_send(node)
54
54
  return if active_resource?(node.parent)
55
-
56
- unless association_without_options?(node)
57
- return if valid_options?(association_with_options?(node))
58
- end
59
-
55
+ return if !association_without_options?(node) && valid_options?(association_with_options?(node))
60
56
  return if valid_options_in_with_options_block?(node)
61
57
 
62
58
  add_offense(node, location: :selector)
@@ -31,6 +31,8 @@ module RuboCop
31
31
  end
32
32
 
33
33
  def on_ivasgn(node)
34
+ return if node.parent.or_asgn_type?
35
+
34
36
  add_offense(node, location: :name)
35
37
  end
36
38
  end
@@ -11,6 +11,7 @@ module RuboCop
11
11
  # @example
12
12
  # # bad
13
13
  # [1, 2, 3].each_with_object({}) { |el, h| h[foo(el)] = el }
14
+ # [1, 2, 3].to_h { |el| [foo(el), el] }
14
15
  # [1, 2, 3].map { |el| [foo(el), el] }.to_h
15
16
  # Hash[[1, 2, 3].collect { |el| [foo(el), el] }]
16
17
  #
@@ -26,6 +27,13 @@ module RuboCop
26
27
  ({send csend} (lvar _memo) :[]= $_ (lvar _el)))
27
28
  PATTERN
28
29
 
30
+ def_node_matcher :on_bad_to_h, <<~PATTERN
31
+ (block
32
+ ({send csend} _ :to_h)
33
+ (args (arg $_el))
34
+ (array $_ (lvar _el)))
35
+ PATTERN
36
+
29
37
  def_node_matcher :on_bad_map_to_h, <<~PATTERN
30
38
  ({send csend}
31
39
  (block
@@ -11,6 +11,7 @@ module RuboCop
11
11
  # @example
12
12
  # # bad
13
13
  # [1, 2, 3].each_with_object({}) { |el, h| h[el] = foo(el) }
14
+ # [1, 2, 3].to_h { |el| [el, foo(el)] }
14
15
  # [1, 2, 3].map { |el| [el, foo(el)] }.to_h
15
16
  # Hash[[1, 2, 3].collect { |el| [el, foo(el)] }]
16
17
  #
@@ -29,6 +30,13 @@ module RuboCop
29
30
  ({send csend} (lvar _memo) :[]= (lvar _el) $_))
30
31
  PATTERN
31
32
 
33
+ def_node_matcher :on_bad_to_h, <<~PATTERN
34
+ (block
35
+ ({send csend} _ :to_h)
36
+ (args (arg $_el))
37
+ (array (lvar _el) $_))
38
+ PATTERN
39
+
32
40
  def_node_matcher :on_bad_map_to_h, <<~PATTERN
33
41
  ({send csend}
34
42
  (block
@@ -26,7 +26,11 @@ module RuboCop
26
26
  MSG = "Prefer Ruby's comparison operators over Active Support's `inquiry`."
27
27
 
28
28
  def on_send(node)
29
- add_offense(node, location: :selector) if node.method?(:inquiry) && node.arguments.empty?
29
+ return unless node.method?(:inquiry) && node.arguments.empty?
30
+ return unless (receiver = node.receiver)
31
+ return if !receiver.str_type? && !receiver.array_type?
32
+
33
+ add_offense(node, location: :selector)
30
34
  end
31
35
  end
32
36
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop checks for places where ordering by `id` column is used.
7
+ #
8
+ # Don't use the `id` column for ordering. The sequence of ids is not guaranteed
9
+ # to be in any particular order, despite often (incidentally) being chronological.
10
+ # Use a timestamp column to order chronologically. As a bonus the intent is clearer.
11
+ #
12
+ # NOTE: Make sure the changed order column does not introduce performance
13
+ # bottlenecks and appropriate database indexes are added.
14
+ #
15
+ # @example
16
+ # # bad
17
+ # scope :chronological, -> { order(id: :asc) }
18
+ # scope :chronological, -> { order(primary_key => :asc) }
19
+ #
20
+ # # good
21
+ # scope :chronological, -> { order(created_at: :asc) }
22
+ #
23
+ class OrderById < Base
24
+ include RangeHelp
25
+
26
+ MSG = 'Do not use the `id` column for ordering. '\
27
+ 'Use a timestamp column to order chronologically.'
28
+
29
+ def_node_matcher :order_by_id?, <<~PATTERN
30
+ (send _ :order
31
+ {
32
+ (sym :id)
33
+ (hash (pair (sym :id) _))
34
+ (send _ :primary_key)
35
+ (hash (pair (send _ :primary_key) _))
36
+ })
37
+ PATTERN
38
+
39
+ def on_send(node)
40
+ return unless node.method?(:order)
41
+
42
+ add_offense(offense_range(node)) if order_by_id?(node)
43
+ end
44
+
45
+ private
46
+
47
+ def offense_range(node)
48
+ range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -9,20 +9,42 @@ module RuboCop
9
9
  # Since `pluck` is an eager method and hits the database immediately,
10
10
  # using `select` helps to avoid additional database queries.
11
11
  #
12
+ # This cop has two different enforcement modes. When the EnforcedStyle
13
+ # is conservative (the default) then only calls to `pluck` on a constant
14
+ # (i.e. a model class) in the `where` is used as offenses.
15
+ #
16
+ # When the EnforcedStyle is aggressive then all calls to `pluck` in the
17
+ # `where` is used as offenses. This may lead to false positives
18
+ # as the cop cannot replace to `select` between calls to `pluck` on an
19
+ # `ActiveRecord::Relation` instance vs a call to `pluck` on an `Array` instance.
20
+ #
12
21
  # @example
13
22
  # # bad
14
23
  # Post.where(user_id: User.active.pluck(:id))
15
24
  #
16
25
  # # good
17
26
  # Post.where(user_id: User.active.select(:id))
27
+ # Post.where(user_id: active_users.select(:id))
28
+ #
29
+ # @example EnforcedStyle: conservative (default)
30
+ # # good
31
+ # Post.where(user_id: active_users.pluck(:id))
32
+ #
33
+ # @example EnforcedStyle: aggressive
34
+ # # bad
35
+ # Post.where(user_id: active_users.pluck(:id))
18
36
  #
19
37
  class PluckInWhere < Cop
20
38
  include ActiveRecordHelper
39
+ include ConfigurableEnforcedStyle
21
40
 
22
41
  MSG = 'Use `select` instead of `pluck` within `where` query method.'
23
42
 
24
43
  def on_send(node)
25
- add_offense(node, location: :selector) if node.method?(:pluck) && in_where?(node)
44
+ return unless node.method?(:pluck) && in_where?(node)
45
+ return if style == :conservative && !root_receiver(node)&.const_type?
46
+
47
+ add_offense(node, location: :selector)
26
48
  end
27
49
 
28
50
  def autocorrect(node)
@@ -30,6 +52,18 @@ module RuboCop
30
52
  corrector.replace(node.loc.selector, 'select')
31
53
  end
32
54
  end
55
+
56
+ private
57
+
58
+ def root_receiver(node)
59
+ receiver = node.receiver
60
+
61
+ if receiver&.send_type?
62
+ root_receiver(receiver)
63
+ else
64
+ receiver
65
+ end
66
+ end
33
67
  end
34
68
  end
35
69
  end
@@ -17,7 +17,7 @@ module RuboCop
17
17
  MSG = 'Use a string value for `class_name`.'
18
18
 
19
19
  def_node_matcher :association_with_reflection, <<~PATTERN
20
- (send nil? {:has_many :has_one :belongs_to} _
20
+ (send nil? {:has_many :has_one :belongs_to} _ _ ?
21
21
  (hash <$#reflection_class_name ...>)
22
22
  )
23
23
  PATTERN
@@ -49,8 +49,7 @@ module RuboCop
49
49
 
50
50
  relative_date?(value) do |method_name|
51
51
  add_offense(node,
52
- location: range_between(name.loc.expression.begin_pos,
53
- value.loc.expression.end_pos),
52
+ location: offense_range(name, value),
54
53
  message: format(MSG, method_name: method_name))
55
54
  end
56
55
  end
@@ -77,6 +76,10 @@ module RuboCop
77
76
 
78
77
  private
79
78
 
79
+ def offense_range(name, value)
80
+ range_between(name.loc.expression.begin_pos, value.loc.expression.end_pos)
81
+ end
82
+
80
83
  def_node_matcher :relative_date_assignment?, <<~PATTERN
81
84
  {
82
85
  (casgn _ _ (send _ ${:since :from_now :after :ago :until :before}))
@@ -129,6 +129,51 @@ module RuboCop
129
129
  # end
130
130
  # end
131
131
  #
132
+ # @example
133
+ # # remove_columns
134
+ #
135
+ # # bad
136
+ # def change
137
+ # remove_columns :users, :name, :email
138
+ # end
139
+ #
140
+ # # good
141
+ # def change
142
+ # reversible do |dir|
143
+ # dir.up do
144
+ # remove_columns :users, :name, :email
145
+ # end
146
+ #
147
+ # dir.down do
148
+ # add_column :users, :name, :string
149
+ # add_column :users, :email, :string
150
+ # end
151
+ # end
152
+ # end
153
+ #
154
+ # # good (Rails >= 6.1, see https://github.com/rails/rails/pull/36589)
155
+ # def change
156
+ # remove_columns :users, :name, :email, type: :string
157
+ # end
158
+ #
159
+ # @example
160
+ # # remove_index
161
+ #
162
+ # # bad
163
+ # def change
164
+ # remove_index :users, name: :index_users_on_email
165
+ # end
166
+ #
167
+ # # good
168
+ # def change
169
+ # remove_index :users, :email
170
+ # end
171
+ #
172
+ # # good
173
+ # def change
174
+ # remove_index :users, column: :email
175
+ # end
176
+ #
132
177
  # @see https://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html
133
178
  class ReversibleMigration < Cop
134
179
  MSG = '%<action>s is not reversible.'
@@ -153,6 +198,14 @@ module RuboCop
153
198
  (send nil? :change_table $_ ...)
154
199
  PATTERN
155
200
 
201
+ def_node_matcher :remove_columns_call, <<~PATTERN
202
+ (send nil? :remove_columns ... $_)
203
+ PATTERN
204
+
205
+ def_node_matcher :remove_index_call, <<~PATTERN
206
+ (send nil? :remove_index _ $_)
207
+ PATTERN
208
+
156
209
  def on_send(node)
157
210
  return unless within_change_method?(node)
158
211
  return if within_reversible_or_up_only_block?(node)
@@ -162,6 +215,8 @@ module RuboCop
162
215
  check_reversible_hash_node(node)
163
216
  check_remove_column_node(node)
164
217
  check_remove_foreign_key_node(node)
218
+ check_remove_columns_node(node)
219
+ check_remove_index_node(node)
165
220
  end
166
221
 
167
222
  def on_block(node)
@@ -237,6 +292,30 @@ module RuboCop
237
292
  end
238
293
  end
239
294
 
295
+ def check_remove_columns_node(node)
296
+ remove_columns_call(node) do |args|
297
+ unless all_hash_key?(args, :type) && target_rails_version >= 6.1
298
+ action = target_rails_version >= 6.1 ? 'remove_columns(without type)' : 'remove_columns'
299
+
300
+ add_offense(
301
+ node,
302
+ message: format(MSG, action: action)
303
+ )
304
+ end
305
+ end
306
+ end
307
+
308
+ def check_remove_index_node(node)
309
+ remove_index_call(node) do |args|
310
+ if args.hash_type? && !all_hash_key?(args, :column)
311
+ add_offense(
312
+ node,
313
+ message: format(MSG, action: 'remove_index(without column)')
314
+ )
315
+ end
316
+ end
317
+ end
318
+
240
319
  def check_change_table_offense(receiver, node)
241
320
  method_name = node.method_name
242
321
  return if receiver != node.receiver &&
@@ -138,7 +138,7 @@ module RuboCop
138
138
  add_offense_for_node(node, CREATE_MSG)
139
139
  end
140
140
 
141
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
141
+ # rubocop:disable Metrics/CyclomaticComplexity
142
142
  def on_send(node)
143
143
  return unless persist_method?(node)
144
144
  return if return_value_assigned?(node)
@@ -150,7 +150,7 @@ module RuboCop
150
150
 
151
151
  add_offense_for_node(node)
152
152
  end
153
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
153
+ # rubocop:enable Metrics/CyclomaticComplexity
154
154
  alias on_csend on_send
155
155
 
156
156
  def autocorrect(node)
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ #
7
+ # Checks SQL heredocs to use `.squish`.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # <<-SQL
12
+ # SELECT * FROM posts;
13
+ # SQL
14
+ #
15
+ # <<-SQL
16
+ # SELECT * FROM posts
17
+ # WHERE id = 1
18
+ # SQL
19
+ #
20
+ # execute(<<~SQL, "Post Load")
21
+ # SELECT * FROM posts
22
+ # WHERE post_id = 1
23
+ # SQL
24
+ #
25
+ # # good
26
+ # <<-SQL.squish
27
+ # SELECT * FROM posts;
28
+ # SQL
29
+ #
30
+ # <<~SQL.squish
31
+ # SELECT * FROM table
32
+ # WHERE id = 1
33
+ # SQL
34
+ #
35
+ # execute(<<~SQL.squish, "Post Load")
36
+ # SELECT * FROM posts
37
+ # WHERE post_id = 1
38
+ # SQL
39
+ #
40
+ class SquishedSQLHeredocs < Cop
41
+ include Heredoc
42
+
43
+ SQL = 'SQL'
44
+ SQUISH = '.squish'
45
+ MSG = 'Use `%<expect>s` instead of `%<current>s`.'
46
+
47
+ def on_heredoc(node)
48
+ return unless offense_detected?(node)
49
+
50
+ add_offense(node)
51
+ end
52
+
53
+ def autocorrect(node)
54
+ lambda do |corrector|
55
+ corrector.insert_after(node, SQUISH)
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def offense_detected?(node)
62
+ sql_heredoc?(node) && !using_squish?(node)
63
+ end
64
+
65
+ def sql_heredoc?(node)
66
+ delimiter_string(node) == SQL
67
+ end
68
+
69
+ def using_squish?(node)
70
+ node.parent&.send_type? && node.parent&.method?(:squish)
71
+ end
72
+
73
+ def message(node)
74
+ format(
75
+ MSG,
76
+ expect: "#{node.source}#{SQUISH}",
77
+ current: node.source
78
+ )
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -18,6 +18,8 @@ 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
  #
@@ -34,23 +34,25 @@ module RuboCop
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
47
-
48
49
  table = schema.table_by(name: table_name(klass))
49
- return true unless table # Skip analysis if it can't find the table
50
-
51
50
  names = column_names(node)
52
- return true unless names
53
51
 
52
+ [klass, table, names]
53
+ end
54
+
55
+ def with_index?(klass, table, names)
54
56
  # Compatibility for Rails 4.2.
55
57
  add_indicies = schema.add_indicies_by(table_name: table_name(klass))
56
58
 
@@ -95,6 +97,8 @@ module RuboCop
95
97
  scope = find_scope(uniq)
96
98
  return unless scope
97
99
 
100
+ scope = unfreeze_scope(scope)
101
+
98
102
  case scope.type
99
103
  when :sym, :str
100
104
  [scope.value]
@@ -112,6 +116,10 @@ module RuboCop
112
116
  end
113
117
  end
114
118
 
119
+ def unfreeze_scope(scope)
120
+ scope.send_type? && scope.method?(:freeze) ? scope.children.first : scope
121
+ end
122
+
115
123
  def class_node(node)
116
124
  node.each_ancestor.find(&:class_type?)
117
125
  end
@@ -3,9 +3,15 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Rails
6
- # This cop enforces the use of `exists?(...)` over `where(...).exists?`.
6
+ # This cop enforces consistent style when using `exists?`.
7
7
  #
8
- # @example
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)
9
15
  # # bad
10
16
  # User.where(name: 'john').exists?
11
17
  # User.where(['name = ?', 'john']).exists?
@@ -17,15 +23,34 @@ module RuboCop
17
23
  # User.where('length(name) > 10').exists?
18
24
  # user.posts.exists?(published: true)
19
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?
20
39
  class WhereExists < Cop
40
+ include ConfigurableEnforcedStyle
41
+
21
42
  MSG = 'Prefer `%<good_method>s` over `%<bad_method>s`.'
22
43
 
23
44
  def_node_matcher :where_exists_call?, <<~PATTERN
24
45
  (send (send _ :where $...) :exists?)
25
46
  PATTERN
26
47
 
48
+ def_node_matcher :exists_with_args?, <<~PATTERN
49
+ (send _ :exists? $...)
50
+ PATTERN
51
+
27
52
  def on_send(node)
28
- where_exists_call?(node) do |args|
53
+ find_offenses(node) do |args|
29
54
  return unless convertable_args?(args)
30
55
 
31
56
  range = correction_range(node)
@@ -35,7 +60,7 @@ module RuboCop
35
60
  end
36
61
 
37
62
  def autocorrect(node)
38
- args = where_exists_call?(node)
63
+ args = find_offenses(node)
39
64
 
40
65
  lambda do |corrector|
41
66
  corrector.replace(
@@ -47,21 +72,59 @@ module RuboCop
47
72
 
48
73
  private
49
74
 
75
+ def where_style?
76
+ style == :where
77
+ end
78
+
79
+ def exists_style?
80
+ style == :exists
81
+ end
82
+
83
+ def find_offenses(node, &block)
84
+ if exists_style?
85
+ where_exists_call?(node, &block)
86
+ elsif where_style?
87
+ exists_with_args?(node, &block)
88
+ end
89
+ end
90
+
50
91
  def convertable_args?(args)
92
+ return false if args.empty?
93
+
51
94
  args.size > 1 || args[0].hash_type? || args[0].array_type?
52
95
  end
53
96
 
54
97
  def correction_range(node)
55
- node.receiver.loc.selector.join(node.loc.selector)
98
+ if exists_style?
99
+ node.receiver.loc.selector.join(node.loc.selector)
100
+ elsif where_style?
101
+ node.loc.selector.with(end_pos: node.loc.expression.end_pos)
102
+ end
56
103
  end
57
104
 
58
105
  def build_good_method(args)
106
+ if exists_style?
107
+ build_good_method_exists(args)
108
+ elsif where_style?
109
+ build_good_method_where(args)
110
+ end
111
+ end
112
+
113
+ def build_good_method_exists(args)
59
114
  if args.size > 1
60
115
  "exists?([#{args.map(&:source).join(', ')}])"
61
116
  else
62
117
  "exists?(#{args[0].source})"
63
118
  end
64
119
  end
120
+
121
+ def build_good_method_where(args)
122
+ if args.size > 1
123
+ "where(#{args.map(&:source).join(', ')}).exists?"
124
+ else
125
+ "where(#{args[0].source}).exists?"
126
+ end
127
+ end
65
128
  end
66
129
  end
67
130
  end
@@ -0,0 +1,106 @@
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.not(...)`.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # User.where('name != ?', 'Gabe')
12
+ # User.where('name != :name', name: 'Gabe')
13
+ # User.where('name IS NOT NULL')
14
+ # User.where('name NOT IN (?)', ['john', 'jane'])
15
+ # User.where('name NOT IN (:names)', names: ['john', 'jane'])
16
+ #
17
+ # # good
18
+ # User.where.not(name: 'Gabe')
19
+ # User.where.not(name: nil)
20
+ # User.where.not(name: ['john', 'jane'])
21
+ #
22
+ class WhereNot < Cop
23
+ include RangeHelp
24
+
25
+ MSG = 'Use `%<good_method>s` instead of manually constructing negated SQL in `where`.'
26
+
27
+ def_node_matcher :where_method_call?, <<~PATTERN
28
+ {
29
+ (send _ :where (array $str_type? $_ ?))
30
+ (send _ :where $str_type? $_ ?)
31
+ }
32
+ PATTERN
33
+
34
+ def on_send(node)
35
+ where_method_call?(node) do |template_node, value_node|
36
+ value_node = value_node.first
37
+
38
+ range = offense_range(node)
39
+
40
+ column_and_value = extract_column_and_value(template_node, value_node)
41
+ return unless column_and_value
42
+
43
+ good_method = build_good_method(*column_and_value)
44
+ message = format(MSG, good_method: good_method)
45
+
46
+ add_offense(node, location: range, message: message)
47
+ end
48
+ end
49
+
50
+ def autocorrect(node)
51
+ where_method_call?(node) do |template_node, value_node|
52
+ value_node = value_node.first
53
+
54
+ lambda do |corrector|
55
+ range = offense_range(node)
56
+
57
+ column, value = *extract_column_and_value(template_node, value_node)
58
+ replacement = build_good_method(column, value)
59
+
60
+ corrector.replace(range, replacement)
61
+ end
62
+ end
63
+ end
64
+
65
+ NOT_EQ_ANONYMOUS_RE = /\A([\w.]+)\s+!=\s+\?\z/.freeze # column != ?
66
+ NOT_IN_ANONYMOUS_RE = /\A([\w.]+)\s+NOT\s+IN\s+\(\?\)\z/i.freeze # column NOT IN (?)
67
+ NOT_EQ_NAMED_RE = /\A([\w.]+)\s+!=\s+:(\w+)\z/.freeze # column != :column
68
+ NOT_IN_NAMED_RE = /\A([\w.]+)\s+NOT\s+IN\s+\(:(\w+)\)\z/i.freeze # column NOT IN (:column)
69
+ IS_NOT_NULL_RE = /\A([\w.]+)\s+IS\s+NOT\s+NULL\z/i.freeze # column IS NOT NULL
70
+
71
+ private
72
+
73
+ def offense_range(node)
74
+ range_between(node.loc.selector.begin_pos, node.loc.expression.end_pos)
75
+ end
76
+
77
+ def extract_column_and_value(template_node, value_node)
78
+ value =
79
+ case template_node.value
80
+ when NOT_EQ_ANONYMOUS_RE, NOT_IN_ANONYMOUS_RE
81
+ value_node.source
82
+ when NOT_EQ_NAMED_RE, NOT_IN_NAMED_RE
83
+ return unless value_node.hash_type?
84
+
85
+ pair = value_node.pairs.find { |p| p.key.value.to_sym == Regexp.last_match(2).to_sym }
86
+ pair.value.source
87
+ when IS_NOT_NULL_RE
88
+ 'nil'
89
+ else
90
+ return
91
+ end
92
+
93
+ [Regexp.last_match(1), value]
94
+ end
95
+
96
+ def build_good_method(column, value)
97
+ if column.include?('.')
98
+ "where.not('#{column}' => #{value})"
99
+ else
100
+ "where.not(#{column}: #{value})"
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -9,6 +9,7 @@ require_relative 'rails/active_record_aliases'
9
9
  require_relative 'rails/active_record_callbacks_order'
10
10
  require_relative 'rails/active_record_override'
11
11
  require_relative 'rails/active_support_aliases'
12
+ require_relative 'rails/after_commit_override'
12
13
  require_relative 'rails/application_controller'
13
14
  require_relative 'rails/application_job'
14
15
  require_relative 'rails/application_mailer'
@@ -48,6 +49,7 @@ require_relative 'rails/mailer_name'
48
49
  require_relative 'rails/match_route'
49
50
  require_relative 'rails/negate_include'
50
51
  require_relative 'rails/not_null_column'
52
+ require_relative 'rails/order_by_id'
51
53
  require_relative 'rails/output'
52
54
  require_relative 'rails/output_safety'
53
55
  require_relative 'rails/pick'
@@ -75,9 +77,11 @@ require_relative 'rails/save_bang'
75
77
  require_relative 'rails/scope_args'
76
78
  require_relative 'rails/short_i18n'
77
79
  require_relative 'rails/skips_model_validations'
80
+ require_relative 'rails/squished_sql_heredocs'
78
81
  require_relative 'rails/time_zone'
79
82
  require_relative 'rails/uniq_before_pluck'
80
83
  require_relative 'rails/unique_validation_without_index'
81
84
  require_relative 'rails/unknown_env'
82
85
  require_relative 'rails/validation'
83
86
  require_relative 'rails/where_exists'
87
+ require_relative 'rails/where_not'
@@ -97,14 +97,12 @@ module RuboCop
97
97
  end.compact
98
98
  end
99
99
 
100
- def each_content(node)
100
+ def each_content(node, &block)
101
101
  return enum_for(__method__, node) unless block_given?
102
102
 
103
103
  case node.body&.type
104
104
  when :begin
105
- node.body.children.each do |child|
106
- yield(child)
107
- end
105
+ node.body.children.each(&block)
108
106
  else
109
107
  yield(node.body)
110
108
  end
@@ -4,7 +4,7 @@ module RuboCop
4
4
  module Rails
5
5
  # This module holds the RuboCop Rails version information.
6
6
  module Version
7
- STRING = '2.7.1'
7
+ STRING = '2.8.0'
8
8
  end
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.1
4
+ version: 2.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bozhidar Batsov
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-07-26 00:00:00.000000000 Z
13
+ date: 2020-09-04 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -77,6 +77,7 @@ files:
77
77
  - lib/rubocop/cop/rails/active_record_callbacks_order.rb
78
78
  - lib/rubocop/cop/rails/active_record_override.rb
79
79
  - lib/rubocop/cop/rails/active_support_aliases.rb
80
+ - lib/rubocop/cop/rails/after_commit_override.rb
80
81
  - lib/rubocop/cop/rails/application_controller.rb
81
82
  - lib/rubocop/cop/rails/application_job.rb
82
83
  - lib/rubocop/cop/rails/application_mailer.rb
@@ -116,6 +117,7 @@ files:
116
117
  - lib/rubocop/cop/rails/match_route.rb
117
118
  - lib/rubocop/cop/rails/negate_include.rb
118
119
  - lib/rubocop/cop/rails/not_null_column.rb
120
+ - lib/rubocop/cop/rails/order_by_id.rb
119
121
  - lib/rubocop/cop/rails/output.rb
120
122
  - lib/rubocop/cop/rails/output_safety.rb
121
123
  - lib/rubocop/cop/rails/pick.rb
@@ -143,12 +145,14 @@ files:
143
145
  - lib/rubocop/cop/rails/scope_args.rb
144
146
  - lib/rubocop/cop/rails/short_i18n.rb
145
147
  - lib/rubocop/cop/rails/skips_model_validations.rb
148
+ - lib/rubocop/cop/rails/squished_sql_heredocs.rb
146
149
  - lib/rubocop/cop/rails/time_zone.rb
147
150
  - lib/rubocop/cop/rails/uniq_before_pluck.rb
148
151
  - lib/rubocop/cop/rails/unique_validation_without_index.rb
149
152
  - lib/rubocop/cop/rails/unknown_env.rb
150
153
  - lib/rubocop/cop/rails/validation.rb
151
154
  - lib/rubocop/cop/rails/where_exists.rb
155
+ - lib/rubocop/cop/rails/where_not.rb
152
156
  - lib/rubocop/cop/rails_cops.rb
153
157
  - lib/rubocop/rails.rb
154
158
  - lib/rubocop/rails/inject.rb