rubocop-dev_doc 0.13.1 → 0.13.2
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 +4 -4
- data/config/default.yml +68 -1
- data/lib/rubocop/cop/dev_doc/hash_pair_removal.rb +52 -0
- data/lib/rubocop/cop/dev_doc/migration/avoid_non_null.rb +4 -2
- data/lib/rubocop/cop/dev_doc/migration/explicit_null_on_reference.rb +85 -0
- data/lib/rubocop/cop/dev_doc/migration/no_bulk_change_table.rb +96 -0
- data/lib/rubocop/cop/dev_doc/migration/redundant_reference_index.rb +104 -0
- data/lib/rubocop/cop/dev_doc/migration/require_reference_foreign_key.rb +109 -0
- data/lib/rubocop/cop/dev_doc/rails/enum_column_not_null.rb +22 -4
- data/lib/rubocop/dev_doc/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5792a89cab7f1814ee9633da4b5c4150e2c281967aa3b718213aeca1d5eea921
|
|
4
|
+
data.tar.gz: fcdb36d319b4aae6389cc665fade58bc7093890249f371d28bd3439e844841e0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5736e1156b6e7535bbe62089bd4d0f197b249df25b0fe4949c4dea682725a536cf211292728398ba68cdb455456a23de761c311f12da00e0b33eb3610e30555
|
|
7
|
+
data.tar.gz: c8a1ecc40eedfd40c9f553a2867a34032bdb37abdbb981204a419036acf9160c0e5b45938718166d84429f65425fd70754c2630f259607dd431017d5d7bd178a
|
data/config/default.yml
CHANGED
|
@@ -293,6 +293,54 @@ DevDoc/Migration/NoCreateJoinTable:
|
|
|
293
293
|
- "db/migrate/*.rb"
|
|
294
294
|
- "db/migrate/**/*.rb"
|
|
295
295
|
|
|
296
|
+
# The exact inverse of Rails/BulkChangeTable (disabled below) — the two must
|
|
297
|
+
# never both be enabled. Whether this gem's `Enabled: false` for the stock cop
|
|
298
|
+
# reaches a project is plugin-load-order dependent (the last-loaded extension's
|
|
299
|
+
# default wins), so projects should ALSO disable Rails/BulkChangeTable
|
|
300
|
+
# explicitly in their own .rubocop.yml.
|
|
301
|
+
# SafeAutoCorrect false: removing `bulk:` turns one combined ALTER TABLE into
|
|
302
|
+
# N separate statements on adapters that support bulk — identical end schema,
|
|
303
|
+
# but a behavior change for unapplied migrations, so it only fires under -A.
|
|
304
|
+
DevDoc/Migration/NoBulkChangeTable:
|
|
305
|
+
Description: "Avoid `bulk:` on `change_table` — a MySQL optimization that is a no-op on PostgreSQL."
|
|
306
|
+
Enabled: true
|
|
307
|
+
SafeAutoCorrect: false
|
|
308
|
+
Include:
|
|
309
|
+
- "db/migrate/*.rb"
|
|
310
|
+
- "db/migrate/**/*.rb"
|
|
311
|
+
|
|
312
|
+
# The reference-options trio. Each of a reference's three options gets the
|
|
313
|
+
# enforcement shape its house position calls for:
|
|
314
|
+
# - foreign_key: has one right value (constrain) -> enforce the VALUE.
|
|
315
|
+
# - null: both values are legitimate -> enforce EXPLICITNESS (either passes).
|
|
316
|
+
# - index: the default is already correct -> enforce SILENCE (flag redundancy).
|
|
317
|
+
# Net effect: everything written on a reference line is a decision, everything
|
|
318
|
+
# omitted is doctrine.
|
|
319
|
+
DevDoc/Migration/RequireReferenceForeignKey:
|
|
320
|
+
Description: "Non-polymorphic references must carry `foreign_key: true` (or a `to_table:` hash)."
|
|
321
|
+
Enabled: true
|
|
322
|
+
Include:
|
|
323
|
+
- "db/migrate/*.rb"
|
|
324
|
+
- "db/migrate/**/*.rb"
|
|
325
|
+
|
|
326
|
+
DevDoc/Migration/ExplicitNullOnReference:
|
|
327
|
+
Description: "References must state `null:` explicitly — optional and mandatory are both legitimate."
|
|
328
|
+
Enabled: true
|
|
329
|
+
Include:
|
|
330
|
+
- "db/migrate/*.rb"
|
|
331
|
+
- "db/migrate/**/*.rb"
|
|
332
|
+
|
|
333
|
+
# Autocorrect is safe here (unlike NoBulkChangeTable): on Migration[5.0]+ the
|
|
334
|
+
# index is created either way, so removing `index: true` cannot change the
|
|
335
|
+
# resulting schema — and Migration[4.2] classes, where the compatibility layer
|
|
336
|
+
# flips the default and the option IS load-bearing, are skipped entirely.
|
|
337
|
+
DevDoc/Migration/RedundantReferenceIndex:
|
|
338
|
+
Description: "`index: true` is redundant on references — they are indexed by default since Rails 5."
|
|
339
|
+
Enabled: true
|
|
340
|
+
Include:
|
|
341
|
+
- "db/migrate/*.rb"
|
|
342
|
+
- "db/migrate/**/*.rb"
|
|
343
|
+
|
|
296
344
|
# This cop is heuristic: it matches column names whose last segment is a known
|
|
297
345
|
# monetary word. Enable it in your project's .rubocop.yml and extend MonetaryNames
|
|
298
346
|
# if your domain uses different names. Disabled by default to avoid false positives.
|
|
@@ -467,10 +515,29 @@ Rails/SaveBang:
|
|
|
467
515
|
# demands trips DevDoc/Migration/AvoidColumnDefault (on the backfill `default:`)
|
|
468
516
|
# and Rails/ReversibleMigration (on `t.change_default`), neither disable-able
|
|
469
517
|
# inline, for the standard add-column-then-drop-default backfill — making the
|
|
470
|
-
# three rules mutually unsatisfiable. Off for all projects
|
|
518
|
+
# three rules mutually unsatisfiable. Off for all projects — and the opposite
|
|
519
|
+
# direction is enforced by DevDoc/Migration/NoBulkChangeTable above, so this
|
|
520
|
+
# must stay off wherever that cop is enabled. NOTE: whether this entry reaches
|
|
521
|
+
# a consumer is plugin-load-order dependent — the last-loaded extension's
|
|
522
|
+
# default wins (measured on rubocop 1.86: `plugins: [rubocop-rails,
|
|
523
|
+
# rubocop-dev_doc]` resolves to false, the reverse order to true) — so
|
|
524
|
+
# projects should disable it explicitly in their own .rubocop.yml.
|
|
471
525
|
Rails/BulkChangeTable:
|
|
472
526
|
Enabled: false
|
|
473
527
|
|
|
528
|
+
# Fires on `add_reference ... null: false` — the exact line complying with
|
|
529
|
+
# DevDoc/Migration/ExplicitNullOnReference produces for a mandatory FK, so the
|
|
530
|
+
# two collide on the same token. Little is lost by turning it off: its
|
|
531
|
+
# add_column half is covered more strictly by DevDoc/Migration/AvoidNonNull,
|
|
532
|
+
# and its add_reference half contradicts house doctrine (mandatory FKs carry
|
|
533
|
+
# `null: false`; a populated-table migration fails loudly at migrate time and
|
|
534
|
+
# uses the add-nullable -> backfill -> tighten pattern instead — note its
|
|
535
|
+
# suggested remedy, `change_column_null`, is itself flagged by AvoidNonNull).
|
|
536
|
+
# Same load-order caveat as Rails/BulkChangeTable above: disable it in the
|
|
537
|
+
# project's .rubocop.yml too.
|
|
538
|
+
Rails/NotNullColumn:
|
|
539
|
+
Enabled: false
|
|
540
|
+
|
|
474
541
|
# Superseded by DevDoc/Style/CaseElseDecision below: same detection, but the
|
|
475
542
|
# offense message carries the raise/report/fall-through decision framework
|
|
476
543
|
# instead of upstream's "Missing else statement" (which teaches the wrong
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module DevDoc
|
|
4
|
+
# Shared autocorrect helper for deleting one pair from a method call's
|
|
5
|
+
# options hash. The removal range is anchored on neighbouring AST nodes
|
|
6
|
+
# rather than walked-over whitespace, so newlines between arguments are
|
|
7
|
+
# handled without corrupting the code.
|
|
8
|
+
#
|
|
9
|
+
# Callers must skip the correction when `pair_removal_range` returns nil
|
|
10
|
+
# or when the range crosses a comment (`crosses_comment?`) — deleting a
|
|
11
|
+
# range that overlaps a comment would corrupt it.
|
|
12
|
+
module HashPairRemoval
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# For a single-pair hash the preceding argument is the anchor (and a
|
|
16
|
+
# braced hash is removed whole); otherwise the neighbouring pair is.
|
|
17
|
+
# A hash containing a `**kwsplat` is bailed on entirely (nil): the
|
|
18
|
+
# anchors navigate `pairs`, which excludes kwsplats, so a splat between
|
|
19
|
+
# anchor and victim would silently fall inside the deleted range.
|
|
20
|
+
def pair_removal_range(node, options, pair)
|
|
21
|
+
return nil unless options.children.size == options.pairs.size
|
|
22
|
+
|
|
23
|
+
if options.pairs.one?
|
|
24
|
+
lone_pair_range(node, options, pair)
|
|
25
|
+
else
|
|
26
|
+
sibling_pair_range(options, pair)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def lone_pair_range(node, options, pair)
|
|
31
|
+
index = node.arguments.index(options)
|
|
32
|
+
return nil if index.zero?
|
|
33
|
+
|
|
34
|
+
victim = options.braces? ? options : pair
|
|
35
|
+
node.arguments[index - 1].source_range.end.join(victim.source_range.end)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def sibling_pair_range(options, pair)
|
|
39
|
+
pairs = options.pairs
|
|
40
|
+
index = pairs.index(pair)
|
|
41
|
+
return pair.source_range.begin.join(pairs[1].source_range.begin) if index.zero?
|
|
42
|
+
|
|
43
|
+
pairs[index - 1].source_range.end.join(pair.source_range.end)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def crosses_comment?(range)
|
|
47
|
+
processed_source.comments.any? { |comment| comment.source_range.overlaps?(range) }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -90,8 +90,10 @@ module RuboCop
|
|
|
90
90
|
#
|
|
91
91
|
# NOTE: This cop only flags `null: false` (and the equivalent `false` arg
|
|
92
92
|
# of `change_column_null`). It does not flag `null: true` (redundant but
|
|
93
|
-
# harmless), and it
|
|
94
|
-
# —
|
|
93
|
+
# harmless on regular columns), and it never requires anything of foreign
|
|
94
|
+
# keys — whether a reference is mandatory is a genuine per-reference
|
|
95
|
+
# decision, so the sibling cop `DevDoc/Migration/ExplicitNullOnReference`
|
|
96
|
+
# requires that decision to be STATED (either value passes).
|
|
95
97
|
#
|
|
96
98
|
# @example
|
|
97
99
|
# # bad
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module DevDoc
|
|
4
|
+
module Migration
|
|
5
|
+
# Every reference must state `null:` explicitly.
|
|
6
|
+
#
|
|
7
|
+
# ## Rationale
|
|
8
|
+
# For regular columns the house rule is nullable
|
|
9
|
+
# (`DevDoc/Migration/AvoidNonNull`), so an absent `null:` is
|
|
10
|
+
# unambiguous — silence IS the decision. Foreign-key columns are the
|
|
11
|
+
# exception that `AvoidNonNull` deliberately carves out: optional and
|
|
12
|
+
# mandatory are both completely legitimate, a genuine per-reference
|
|
13
|
+
# business decision. But a legitimate decision that is never written down is
|
|
14
|
+
# indistinguishable from an omission — a bare reference reads as "did
|
|
15
|
+
# the developer forget?".
|
|
16
|
+
#
|
|
17
|
+
# This cop requires the decision to be stated. Either value passes;
|
|
18
|
+
# `null: true` is not redundant here, it is the self-documenting
|
|
19
|
+
# spelling of "optional by design".
|
|
20
|
+
#
|
|
21
|
+
# ❌ Ambiguous — optional by design, or forgotten?
|
|
22
|
+
# t.belongs_to :organization, foreign_key: true
|
|
23
|
+
#
|
|
24
|
+
# ✔️ Mandatory, stated
|
|
25
|
+
# t.belongs_to :organization, foreign_key: true, null: false
|
|
26
|
+
#
|
|
27
|
+
# ✔️ Optional, stated
|
|
28
|
+
# t.belongs_to :organization, foreign_key: true, null: true
|
|
29
|
+
#
|
|
30
|
+
# Polymorphic references are held to the same rule — their
|
|
31
|
+
# optionality is just as much a decision.
|
|
32
|
+
#
|
|
33
|
+
# ## Relationship with Rails/NotNullColumn
|
|
34
|
+
# The stock cop flags `add_reference ... null: false` (NOT NULL added
|
|
35
|
+
# to a possibly-populated table without a default) — the exact line
|
|
36
|
+
# complying with THIS cop produces for a mandatory FK, so the two
|
|
37
|
+
# collide. Consumers disable `Rails/NotNullColumn`: its `add_column`
|
|
38
|
+
# half is covered more strictly by `DevDoc/Migration/AvoidNonNull`,
|
|
39
|
+
# and its `add_reference` half contradicts house doctrine (a
|
|
40
|
+
# populated-table migration fails loudly at migrate time and uses the
|
|
41
|
+
# add-nullable -> backfill -> tighten pattern instead). This gem's
|
|
42
|
+
# default config disables it, but that propagation is load-order
|
|
43
|
+
# dependent (see `NoBulkChangeTable`'s docstring) — disable it in the
|
|
44
|
+
# project's `.rubocop.yml` too.
|
|
45
|
+
#
|
|
46
|
+
# NOTE: This cop enforces that optionality is stated; the sibling cop
|
|
47
|
+
# `DevDoc/Migration/RequireReferenceForeignKey` enforces the
|
|
48
|
+
# constraint's presence, and
|
|
49
|
+
# `DevDoc/Migration/RedundantReferenceIndex` removes the one reference
|
|
50
|
+
# option whose default is already correct. Together: everything
|
|
51
|
+
# written on a reference is a decision, everything omitted is
|
|
52
|
+
# doctrine.
|
|
53
|
+
#
|
|
54
|
+
# @example
|
|
55
|
+
# # bad
|
|
56
|
+
# t.belongs_to :user, foreign_key: true
|
|
57
|
+
#
|
|
58
|
+
# # good
|
|
59
|
+
# t.belongs_to :user, foreign_key: true, null: false
|
|
60
|
+
#
|
|
61
|
+
# # good
|
|
62
|
+
# t.belongs_to :user, foreign_key: true, null: true
|
|
63
|
+
class ExplicitNullOnReference < Base
|
|
64
|
+
MSG = 'State `null:` explicitly — optional and mandatory are both legitimate ' \
|
|
65
|
+
'for a reference, so omission reads as an unmade decision.'.freeze
|
|
66
|
+
|
|
67
|
+
RESTRICT_ON_SEND = %i[belongs_to references add_reference add_belongs_to].freeze
|
|
68
|
+
|
|
69
|
+
def on_send(node)
|
|
70
|
+
options = node.arguments.find(&:hash_type?)
|
|
71
|
+
return if options && null_stated?(options)
|
|
72
|
+
|
|
73
|
+
add_offense(node.loc.selector)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def null_stated?(options)
|
|
79
|
+
options.pairs.any? { |pair| pair.key.sym_type? && pair.key.value == :null }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require_relative '../hash_pair_removal'
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module DevDoc
|
|
6
|
+
module Migration
|
|
7
|
+
# Avoid the `bulk:` option on `change_table`.
|
|
8
|
+
#
|
|
9
|
+
# ## Rationale
|
|
10
|
+
# `bulk: true` combines multiple ALTER TABLE sub-commands into a single
|
|
11
|
+
# statement. That is a MySQL/MariaDB optimization — it avoids repeated
|
|
12
|
+
# full-table rewrites there — but consuming projects run PostgreSQL,
|
|
13
|
+
# where ALTER sub-commands are cheap metadata operations and `bulk:` is
|
|
14
|
+
# a no-op that only adds noise.
|
|
15
|
+
#
|
|
16
|
+
# Worse, the combined form conflicts with the standard
|
|
17
|
+
# add-column-then-drop-default backfill pattern: inside a `bulk: true`
|
|
18
|
+
# block it trips `DevDoc/Migration/AvoidColumnDefault` (on the backfill
|
|
19
|
+
# `default:`) and `Rails/ReversibleMigration` (on `t.change_default`),
|
|
20
|
+
# neither of which can be disabled inline.
|
|
21
|
+
#
|
|
22
|
+
# ❌
|
|
23
|
+
# change_table :users, bulk: true do |t|
|
|
24
|
+
# t.string :token
|
|
25
|
+
# t.datetime :closed_at
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# ✔️
|
|
29
|
+
# change_table :users do |t|
|
|
30
|
+
# t.string :token
|
|
31
|
+
# t.datetime :closed_at
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# ## Relationship with Rails/BulkChangeTable
|
|
35
|
+
# This cop is the exact inverse of `Rails/BulkChangeTable`, which
|
|
36
|
+
# DEMANDS `bulk: true` whenever several ALTERs touch one table. The two
|
|
37
|
+
# cannot both be enabled. This gem's default config disables
|
|
38
|
+
# `Rails/BulkChangeTable`, but whether that default reaches a project
|
|
39
|
+
# is plugin-load-order dependent — the last-loaded extension's default
|
|
40
|
+
# wins, so a project listing `rubocop-dev_doc` before `rubocop-rails`
|
|
41
|
+
# still gets rubocop-rails' `Enabled: true`. Disable it explicitly in
|
|
42
|
+
# the project's `.rubocop.yml` rather than relying on load order.
|
|
43
|
+
#
|
|
44
|
+
# NOTE: When a comment sits between `change_table`'s arguments, the cop
|
|
45
|
+
# flags the option but does not autocorrect (removing the range would
|
|
46
|
+
# corrupt the comment) — delete the option manually.
|
|
47
|
+
#
|
|
48
|
+
# @example
|
|
49
|
+
# # bad
|
|
50
|
+
# change_table :users, bulk: true do |t|
|
|
51
|
+
# t.string :token
|
|
52
|
+
# end
|
|
53
|
+
#
|
|
54
|
+
# # good
|
|
55
|
+
# change_table :users do |t|
|
|
56
|
+
# t.string :token
|
|
57
|
+
# end
|
|
58
|
+
class NoBulkChangeTable < Base
|
|
59
|
+
extend AutoCorrector
|
|
60
|
+
include HashPairRemoval
|
|
61
|
+
|
|
62
|
+
MSG = 'Avoid `bulk:` on `change_table` — a MySQL optimization that is a no-op on PostgreSQL.'.freeze
|
|
63
|
+
RESTRICT_ON_SEND = %i[change_table].freeze
|
|
64
|
+
|
|
65
|
+
def on_send(node)
|
|
66
|
+
options = node.arguments.find(&:hash_type?)
|
|
67
|
+
return unless options
|
|
68
|
+
|
|
69
|
+
options.pairs.each { |pair| check_pair(node, options, pair) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def check_pair(node, options, pair)
|
|
75
|
+
return unless bulk_key?(pair.key)
|
|
76
|
+
|
|
77
|
+
range = pair_removal_range(node, options, pair)
|
|
78
|
+
if range.nil? || crosses_comment?(range)
|
|
79
|
+
add_offense(pair)
|
|
80
|
+
else
|
|
81
|
+
add_offense(pair) { |corrector| corrector.remove(range) }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def bulk_key?(key)
|
|
86
|
+
(key.sym_type? || key.str_type?) && key.value.to_s == 'bulk'
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def crosses_comment?(range)
|
|
90
|
+
processed_source.comments.any? { |comment| comment.source_range.overlaps?(range) }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
require_relative '../hash_pair_removal'
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module DevDoc
|
|
6
|
+
module Migration
|
|
7
|
+
# Avoid the redundant `index: true` on references.
|
|
8
|
+
#
|
|
9
|
+
# ## Rationale
|
|
10
|
+
# `belongs_to` / `references` / `add_reference` default to
|
|
11
|
+
# `index: true` since Rails 5 — the index is created whether or not
|
|
12
|
+
# the option is written. An explicit `index: true` therefore does
|
|
13
|
+
# nothing except make every future reader wonder whether it does
|
|
14
|
+
# (and imply, wrongly, that references without it are unindexed).
|
|
15
|
+
#
|
|
16
|
+
# The option is only meaningful when it *overrides* the default:
|
|
17
|
+
# `index: false` to opt out, or a hash to customize
|
|
18
|
+
# (`index: { unique: true }`). Those forms pass untouched.
|
|
19
|
+
#
|
|
20
|
+
# ❌ Redundant — this index exists anyway
|
|
21
|
+
# t.belongs_to :user, foreign_key: true, null: false, index: true
|
|
22
|
+
#
|
|
23
|
+
# ✔️
|
|
24
|
+
# t.belongs_to :user, foreign_key: true, null: false
|
|
25
|
+
#
|
|
26
|
+
# ✔️ Meaningful override
|
|
27
|
+
# t.belongs_to :user, foreign_key: true, null: false, index: { unique: true }
|
|
28
|
+
#
|
|
29
|
+
# NOTE: This cop removes the one reference option whose default is
|
|
30
|
+
# already correct; the sibling cops
|
|
31
|
+
# `DevDoc/Migration/RequireReferenceForeignKey` and
|
|
32
|
+
# `DevDoc/Migration/ExplicitNullOnReference` require the two options
|
|
33
|
+
# that ARE decisions to be made and stated. Together: everything
|
|
34
|
+
# written on a reference is a decision, everything omitted is
|
|
35
|
+
# doctrine.
|
|
36
|
+
#
|
|
37
|
+
# NOTE: When a comment sits between the call's arguments, the cop
|
|
38
|
+
# flags the option but does not autocorrect (removing the range would
|
|
39
|
+
# corrupt the comment) — delete the option manually.
|
|
40
|
+
#
|
|
41
|
+
# NOTE: Migrations declared `< ActiveRecord::Migration[4.2]` are
|
|
42
|
+
# skipped entirely: the 4.2 compatibility layer flips the reference
|
|
43
|
+
# default to `index: false`, so there `index: true` is meaningful —
|
|
44
|
+
# removing it would drop the index on a from-zero `db:migrate`.
|
|
45
|
+
#
|
|
46
|
+
# @example
|
|
47
|
+
# # bad
|
|
48
|
+
# t.belongs_to :user, foreign_key: true, index: true
|
|
49
|
+
#
|
|
50
|
+
# # good
|
|
51
|
+
# t.belongs_to :user, foreign_key: true
|
|
52
|
+
#
|
|
53
|
+
# # good (meaningful override)
|
|
54
|
+
# t.belongs_to :user, foreign_key: true, index: { unique: true }
|
|
55
|
+
class RedundantReferenceIndex < Base
|
|
56
|
+
extend AutoCorrector
|
|
57
|
+
include HashPairRemoval
|
|
58
|
+
|
|
59
|
+
MSG = '`index: true` is redundant — references are indexed by default since Rails 5.'.freeze
|
|
60
|
+
|
|
61
|
+
RESTRICT_ON_SEND = %i[belongs_to references add_reference add_belongs_to].freeze
|
|
62
|
+
|
|
63
|
+
def on_send(node)
|
|
64
|
+
options = node.arguments.find(&:hash_type?)
|
|
65
|
+
return unless options
|
|
66
|
+
return if legacy_migration?(node)
|
|
67
|
+
|
|
68
|
+
options.pairs.each { |pair| check_pair(node, options, pair) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# `ActiveRecord::Migration[4.2]` compatibility flips the reference
|
|
74
|
+
# default to `index: false` (see compatibility.rb's V4_2), so inside
|
|
75
|
+
# such a class `index: true` is load-bearing, not redundant.
|
|
76
|
+
def legacy_migration?(node)
|
|
77
|
+
node.each_ancestor(:class).any? do |klass|
|
|
78
|
+
superclass = klass.children[1]
|
|
79
|
+
next false unless superclass&.send_type? && superclass.method?(:[])
|
|
80
|
+
|
|
81
|
+
version = superclass.first_argument
|
|
82
|
+
version&.numeric_type? && version.value < 5.0
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def check_pair(node, options, pair)
|
|
87
|
+
return unless index_key?(pair.key) && pair.value.true_type?
|
|
88
|
+
|
|
89
|
+
range = pair_removal_range(node, options, pair)
|
|
90
|
+
if range.nil? || crosses_comment?(range)
|
|
91
|
+
add_offense(pair)
|
|
92
|
+
else
|
|
93
|
+
add_offense(pair) { |corrector| corrector.remove(range) }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def index_key?(key)
|
|
98
|
+
(key.sym_type? || key.str_type?) && key.value.to_s == 'index'
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module DevDoc
|
|
4
|
+
module Migration
|
|
5
|
+
# Every non-polymorphic reference must carry a truthy `foreign_key:`.
|
|
6
|
+
#
|
|
7
|
+
# ## Rationale
|
|
8
|
+
# `foreign_key: true` (or a `to_table:` hash) is pure referential
|
|
9
|
+
# integrity — never a business decision — so the house rule is that
|
|
10
|
+
# every reference carries the DB-level constraint. Rails defaults
|
|
11
|
+
# references to NO constraint, which means both an omitted option and
|
|
12
|
+
# an explicit `foreign_key: false` silently ship an unconstrained
|
|
13
|
+
# column, indistinguishable from an oversight.
|
|
14
|
+
#
|
|
15
|
+
# This cop makes the constraint the enforced default and turns the
|
|
16
|
+
# rare deliberate exception into a documented, greppable site: keep
|
|
17
|
+
# `foreign_key: false` on the line and disable this cop with a brief
|
|
18
|
+
# reason.
|
|
19
|
+
#
|
|
20
|
+
# ❌ Omitted — reads as "never decided"
|
|
21
|
+
# t.belongs_to :author, null: false
|
|
22
|
+
#
|
|
23
|
+
# ❌ Explicit false without justification
|
|
24
|
+
# t.belongs_to :author, null: false, foreign_key: false
|
|
25
|
+
#
|
|
26
|
+
# ✔️
|
|
27
|
+
# t.belongs_to :author, null: false, foreign_key: true
|
|
28
|
+
#
|
|
29
|
+
# ✔️ Cross-table naming
|
|
30
|
+
# add_reference :posts, :reviewer, foreign_key: { to_table: :users }
|
|
31
|
+
#
|
|
32
|
+
# ✔️ Deliberate exception, self-documenting
|
|
33
|
+
# # rubocop:disable DevDoc/Migration/RequireReferenceForeignKey -- append-only audit table
|
|
34
|
+
# t.belongs_to :snapshot, null: false, foreign_key: false
|
|
35
|
+
# # rubocop:enable DevDoc/Migration/RequireReferenceForeignKey
|
|
36
|
+
#
|
|
37
|
+
# ## Exception
|
|
38
|
+
# Polymorphic references (`polymorphic: true`) are skipped — a
|
|
39
|
+
# polymorphic column points at many tables, so no single FK constraint
|
|
40
|
+
# can exist.
|
|
41
|
+
#
|
|
42
|
+
# NOTE: A reference whose constraint is added separately via
|
|
43
|
+
# `add_foreign_key` later in the migration (the pattern engine-vendored
|
|
44
|
+
# migrations use) is still flagged — the cop cannot reliably match the
|
|
45
|
+
# two up. Prefer the inline `foreign_key:` option; for vendored engine
|
|
46
|
+
# migrations, disable with a reason or exclude the file.
|
|
47
|
+
#
|
|
48
|
+
# NOTE: This cop enforces the constraint's presence; the sibling cop
|
|
49
|
+
# `DevDoc/Migration/ExplicitNullOnReference` enforces that the
|
|
50
|
+
# column's optionality is stated, and
|
|
51
|
+
# `DevDoc/Migration/RedundantReferenceIndex` removes the one reference
|
|
52
|
+
# option whose default is already correct. Together: everything
|
|
53
|
+
# written on a reference is a decision, everything omitted is
|
|
54
|
+
# doctrine.
|
|
55
|
+
#
|
|
56
|
+
# @example
|
|
57
|
+
# # bad
|
|
58
|
+
# t.belongs_to :author
|
|
59
|
+
#
|
|
60
|
+
# # bad
|
|
61
|
+
# t.belongs_to :author, foreign_key: false
|
|
62
|
+
#
|
|
63
|
+
# # good
|
|
64
|
+
# t.belongs_to :author, foreign_key: true
|
|
65
|
+
#
|
|
66
|
+
# # good (polymorphic — no constraint possible)
|
|
67
|
+
# t.belongs_to :item, polymorphic: true
|
|
68
|
+
class RequireReferenceForeignKey < Base
|
|
69
|
+
MSG = 'Add `foreign_key: true` (or a `to_table:` hash); a deliberate exception ' \
|
|
70
|
+
'keeps `foreign_key: false` plus a reasoned disable of this cop.'.freeze
|
|
71
|
+
|
|
72
|
+
RESTRICT_ON_SEND = %i[belongs_to references add_reference add_belongs_to].freeze
|
|
73
|
+
|
|
74
|
+
def on_send(node)
|
|
75
|
+
options = node.arguments.find(&:hash_type?)
|
|
76
|
+
return if polymorphic?(options)
|
|
77
|
+
|
|
78
|
+
pair = foreign_key_pair(options)
|
|
79
|
+
if pair.nil?
|
|
80
|
+
add_offense(node.loc.selector)
|
|
81
|
+
elsif pair.value.false_type? || pair.value.nil_type?
|
|
82
|
+
add_offense(pair)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# Any `polymorphic:` value other than literal false/nil counts as
|
|
89
|
+
# polymorphic — a non-literal value can't be resolved statically, and
|
|
90
|
+
# demanding an impossible FK would be a worse failure than skipping.
|
|
91
|
+
def polymorphic?(options)
|
|
92
|
+
return false unless options
|
|
93
|
+
|
|
94
|
+
options.pairs.any? do |pair|
|
|
95
|
+
pair.key.sym_type? && pair.key.value == :polymorphic &&
|
|
96
|
+
!pair.value.false_type? && !pair.value.nil_type?
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def foreign_key_pair(options)
|
|
101
|
+
return nil unless options
|
|
102
|
+
|
|
103
|
+
options.pairs.find { |pair| pair.key.sym_type? && pair.key.value == :foreign_key }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -38,8 +38,16 @@ module RuboCop
|
|
|
38
38
|
# (e.g. projects using `structure.sql`). It also relies on the schema
|
|
39
39
|
# being current, resolves the table name by Rails convention (STI,
|
|
40
40
|
# `self.table_name` overrides, or namespaced models may not resolve),
|
|
41
|
-
# recognizes
|
|
42
|
-
#
|
|
41
|
+
# recognizes the positional `enum :name, …` form (and glib's
|
|
42
|
+
# `enum_symbolize`; see below), and skips silently if the backing
|
|
43
|
+
# column cannot be found in the schema.
|
|
44
|
+
#
|
|
45
|
+
# Glib's `enum_symbolize :name, { … }` (which declares the enum itself)
|
|
46
|
+
# is recognized too — its presence validation guards the app layer, but
|
|
47
|
+
# this cop is about the DB-level guarantee, which validations cannot
|
|
48
|
+
# provide (`update_column`, `update_all`, `insert_all`, raw SQL). The
|
|
49
|
+
# legacy two-line form (`enum :name, …` + `enum_symbolize :name`) is
|
|
50
|
+
# covered via its bare `enum` line.
|
|
43
51
|
#
|
|
44
52
|
# @example
|
|
45
53
|
# # bad - the `status` column is nullable in db/schema.rb
|
|
@@ -51,16 +59,26 @@ module RuboCop
|
|
|
51
59
|
# class Order < ApplicationRecord
|
|
52
60
|
# enum :status, { active: 0, archived: 1 }
|
|
53
61
|
# end
|
|
62
|
+
#
|
|
63
|
+
# @example Glib's `enum_symbolize`
|
|
64
|
+
# # bad - the same rule applies; the column must still be `null: false`
|
|
65
|
+
# class Order < ApplicationRecord
|
|
66
|
+
# enum_symbolize :status, { active: 0, archived: 1 }
|
|
67
|
+
# end
|
|
54
68
|
class EnumColumnNotNull < Base
|
|
55
69
|
include ActiveRecordHelper
|
|
56
70
|
|
|
57
71
|
MSG = 'Enum column `%<name>s` should be `null: false` — NULL is outside an enum\'s domain. ' \
|
|
58
72
|
'Model "unset" as an explicit enum value.'.freeze
|
|
59
73
|
|
|
60
|
-
RESTRICT_ON_SEND = %i[enum].freeze
|
|
74
|
+
RESTRICT_ON_SEND = %i[enum enum_symbolize].freeze
|
|
61
75
|
|
|
76
|
+
# `enum_symbolize` matches only its full (value-declaring) form; the
|
|
77
|
+
# legacy `enum_symbolize :name` (no values) is not an enum declaration —
|
|
78
|
+
# the bare `enum` line above it is, and is matched separately.
|
|
62
79
|
def_node_matcher :enum_call, <<~PATTERN
|
|
63
|
-
(send nil? :enum (sym $_) ...)
|
|
80
|
+
{(send nil? :enum (sym $_) ...)
|
|
81
|
+
(send nil? :enum_symbolize (sym $_) {hash array} ...)}
|
|
64
82
|
PATTERN
|
|
65
83
|
|
|
66
84
|
def on_send(node)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubocop-dev_doc
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.13.
|
|
4
|
+
version: 0.13.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dev-doc contributors
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -110,6 +110,7 @@ files:
|
|
|
110
110
|
- lib/rubocop/cop/dev_doc/auth/no_record_presence_in_policy.rb
|
|
111
111
|
- lib/rubocop/cop/dev_doc/auth/role_predicate_outside_policy.rb
|
|
112
112
|
- lib/rubocop/cop/dev_doc/auth/unscoped_find_justification.rb
|
|
113
|
+
- lib/rubocop/cop/dev_doc/hash_pair_removal.rb
|
|
113
114
|
- lib/rubocop/cop/dev_doc/i18n/avoid_titleize_humanize.rb
|
|
114
115
|
- lib/rubocop/cop/dev_doc/i18n/localizable_props.rb
|
|
115
116
|
- lib/rubocop/cop/dev_doc/i18n/report_text.rb
|
|
@@ -124,9 +125,13 @@ files:
|
|
|
124
125
|
- lib/rubocop/cop/dev_doc/migration/avoid_vague_column_names.rb
|
|
125
126
|
- lib/rubocop/cop/dev_doc/migration/boolean_column_not_null.rb
|
|
126
127
|
- lib/rubocop/cop/dev_doc/migration/date_column_naming.rb
|
|
128
|
+
- lib/rubocop/cop/dev_doc/migration/explicit_null_on_reference.rb
|
|
129
|
+
- lib/rubocop/cop/dev_doc/migration/no_bulk_change_table.rb
|
|
127
130
|
- lib/rubocop/cop/dev_doc/migration/no_create_join_table.rb
|
|
128
131
|
- lib/rubocop/cop/dev_doc/migration/prefer_belongs_to.rb
|
|
132
|
+
- lib/rubocop/cop/dev_doc/migration/redundant_reference_index.rb
|
|
129
133
|
- lib/rubocop/cop/dev_doc/migration/require_primary_key.rb
|
|
134
|
+
- lib/rubocop/cop/dev_doc/migration/require_reference_foreign_key.rb
|
|
130
135
|
- lib/rubocop/cop/dev_doc/migration/require_timestamps.rb
|
|
131
136
|
- lib/rubocop/cop/dev_doc/rails/application_record_transaction.rb
|
|
132
137
|
- lib/rubocop/cop/dev_doc/rails/avoid_bypassing_validation.rb
|