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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c36d2d77bcdffd7f74968817e83dbdb75134e3e3d63b953127e4475f7c1841f9
4
- data.tar.gz: db6ef7a22d41f10f30e6e6ffedfdb1c7605bc849907290930f9d1eae4f0c49b4
3
+ metadata.gz: 5792a89cab7f1814ee9633da4b5c4150e2c281967aa3b718213aeca1d5eea921
4
+ data.tar.gz: fcdb36d319b4aae6389cc665fade58bc7093890249f371d28bd3439e844841e0
5
5
  SHA512:
6
- metadata.gz: a874f22bc4773c0abaeb6de19587b5c1934061d40fb608831b2c725d060acdc39c8817689f38cab35041d1a67d99cfc12daf995a4bc3780fcc91a4f99901dfcb
7
- data.tar.gz: 9966867628667e9fee6dd869345d1943e936db024a426b6dbd7433f14ae93396c5d8edeffba3a97b1259aac8f831ada7f288d93fdfceb14c34b4893d03fe613d
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 does not require foreign keys to carry `null: false`
94
- # — adding it to an FK is encouraged but not enforced here.
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 only the positional `enum :name, …` form, and skips
42
- # silently if the backing column cannot be found in the schema.
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)
@@ -1,5 +1,5 @@
1
1
  module RuboCop
2
2
  module DevDoc
3
- VERSION = "0.13.1".freeze
3
+ VERSION = "0.13.2".freeze
4
4
  end
5
5
  end
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.1
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-07-25 00:00:00.000000000 Z
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