rubocop-rails 2.7.1 → 2.8.0

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