rubocop-dev_doc 0.5.0.beta1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/config/default.yml +19 -139
- data/lib/rubocop/cop/dev_doc/auth/current_user_branching.rb +47 -4
- data/lib/rubocop/cop/dev_doc/migration/avoid_column_default.rb +53 -2
- data/lib/rubocop/cop/dev_doc/migration/avoid_vague_column_names.rb +7 -1
- data/lib/rubocop/cop/dev_doc/rails/avoid_ordering_by_id.rb +167 -0
- data/lib/rubocop/cop/dev_doc/rails/avoid_rails_callbacks.rb +14 -4
- data/lib/rubocop/cop/dev_doc/rails/avoid_raw_sql.rb +227 -0
- data/lib/rubocop/cop/dev_doc/style/prefer_public_send.rb +86 -0
- data/lib/rubocop/cop/dev_doc/style/tap_block_ignores_value.rb +123 -0
- data/lib/rubocop/cop/dev_doc/test/avoid_unit_test.rb +24 -0
- data/lib/rubocop/dev_doc/version.rb +1 -1
- metadata +13 -7
- data/lib/rubocop/cop/dev_doc/i18n/report_text.rb +0 -112
- data/lib/rubocop/cop/dev_doc/i18n/require_translation.rb +0 -116
- data/lib/rubocop/cop/dev_doc/i18n/translation_key_prefix.rb +0 -89
- data/lib/rubocop/cop/dev_doc/i18n/unverified_translation.rb +0 -106
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2076123d07503e775a29287eafcd8dc9d8cf6e344eeb4b462d29120fdef12879
|
|
4
|
+
data.tar.gz: 910414d217e37c808b7626add2b876d24f6ef214a725b373e86dc736acb24ff7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7681307dae1b8d09d0344c199d4be497bc4c986b4314711ae39550d7d8ced8d8b811d07d98997f8e3fc85d294e6fde1b97c8cb6cdc3c33a40ad3074cda985d62
|
|
7
|
+
data.tar.gz: a047b648abfcaeb1842158a562c17028601a2f24c67e1f0aa2cdc548377cccd1658dae4b6b68cee1cb4906275a59822f2870160380e71cb5d730c04ce5d44196
|
data/config/default.yml
CHANGED
|
@@ -7,10 +7,6 @@ DevDoc/Auth:
|
|
|
7
7
|
DocumentationBaseURL: https://github.com/hgani/dev-doc/blob/main/docs/cops
|
|
8
8
|
DocumentationExtension: ".md"
|
|
9
9
|
|
|
10
|
-
DevDoc/I18n:
|
|
11
|
-
DocumentationBaseURL: https://github.com/hgani/dev-doc/blob/main/docs/cops
|
|
12
|
-
DocumentationExtension: ".md"
|
|
13
|
-
|
|
14
10
|
DevDoc/Migration:
|
|
15
11
|
DocumentationBaseURL: https://github.com/hgani/dev-doc/blob/main/docs/cops
|
|
16
12
|
DocumentationExtension: ".md"
|
|
@@ -83,11 +79,12 @@ DevDoc/Migration/DateColumnNaming:
|
|
|
83
79
|
- "db/migrate/**/*.rb"
|
|
84
80
|
|
|
85
81
|
DevDoc/Migration/AvoidVagueColumnNames:
|
|
86
|
-
Description: "Avoid vague column names like `status` or `
|
|
82
|
+
Description: "Avoid vague column names like `status`, `group`, or `kind`. Use more specific names."
|
|
87
83
|
Enabled: true
|
|
88
84
|
VagueNames:
|
|
89
85
|
- status
|
|
90
86
|
- group
|
|
87
|
+
- kind
|
|
91
88
|
Include:
|
|
92
89
|
- "db/migrate/*.rb"
|
|
93
90
|
- "db/migrate/**/*.rb"
|
|
@@ -150,10 +147,10 @@ DevDoc/Rails/EnumColumnNotNull:
|
|
|
150
147
|
DevDoc/Rails/NoBlockPredicateOnRelation:
|
|
151
148
|
Description: "Avoid block-form `count`/`reject`/`select`/`find`/`any?` on AR relations; push the predicate into SQL with `.where(...)` or a scope."
|
|
152
149
|
Enabled: true
|
|
150
|
+
AdditionalNonRelationMethods: []
|
|
153
151
|
Exclude:
|
|
154
152
|
- "spec/**/*"
|
|
155
153
|
- "test/**/*"
|
|
156
|
-
- "db/seeds.rb"
|
|
157
154
|
- "db/seeds/**/*.rb"
|
|
158
155
|
- "lib/tasks/**/*.rb"
|
|
159
156
|
|
|
@@ -169,6 +166,10 @@ DevDoc/Style/AvoidSend:
|
|
|
169
166
|
Description: "Avoid `send`/`public_send` with an explicit receiver; prefer direct calls or safer alternatives."
|
|
170
167
|
Enabled: true
|
|
171
168
|
|
|
169
|
+
DevDoc/Style/PreferPublicSend:
|
|
170
|
+
Description: "Prefer `public_send` over `send` — visibility-respecting dispatch surfaces typos/misconfig and documents intent."
|
|
171
|
+
Enabled: true
|
|
172
|
+
|
|
172
173
|
DevDoc/Style/RepeatedSafeNavigationReceiver:
|
|
173
174
|
Description: "Avoid using `&.` on the same receiver more than once in a method body — assign once and use `.` after."
|
|
174
175
|
Enabled: true
|
|
@@ -181,6 +182,10 @@ DevDoc/Style/MinimizeVariableScope:
|
|
|
181
182
|
Description: "Assign a variable inside the `if` condition that guards it, to keep its scope local to the branch."
|
|
182
183
|
Enabled: true
|
|
183
184
|
|
|
185
|
+
DevDoc/Style/TapBlockIgnoresValue:
|
|
186
|
+
Description: "Flag `tap` whose block ignores the yielded value — `tap` is for operating on the receiver, not eliding a temp var."
|
|
187
|
+
Enabled: true
|
|
188
|
+
|
|
184
189
|
DevDoc/Style/AvoidHeadResponse:
|
|
185
190
|
Description: "Avoid `head()` with error statuses; delegate error handling to Rails exceptions or model validations."
|
|
186
191
|
Enabled: true
|
|
@@ -311,6 +316,14 @@ DevDoc/Rails/StrongParametersExpect:
|
|
|
311
316
|
Include:
|
|
312
317
|
- "app/controllers/**/*.rb"
|
|
313
318
|
|
|
319
|
+
DevDoc/Rails/AvoidRawSql:
|
|
320
|
+
Description: "Avoid raw SQL — prefer the hash/array form, parameterized fragments, or Arel; disable with a reason when raw SQL is genuinely required."
|
|
321
|
+
Enabled: true
|
|
322
|
+
|
|
323
|
+
DevDoc/Rails/AvoidOrderingById:
|
|
324
|
+
Description: "Avoid `order`/`reorder` by `id` as the primary (sole or leftmost) sort — order by a business column, or use `id` only as a secondary tiebreaker."
|
|
325
|
+
Enabled: true
|
|
326
|
+
|
|
314
327
|
# `save!` / `update!` raise on persistence failure, which is what
|
|
315
328
|
# we want in jobs/services/rake tasks — silent `false` returns
|
|
316
329
|
# hide bugs. Excluded for controllers, which lean on `save`
|
|
@@ -383,136 +396,3 @@ DevDoc/Auth/CurrentUserBranching:
|
|
|
383
396
|
- "app/helpers/**/*.rb"
|
|
384
397
|
- "app/controllers/concerns/**/*.rb"
|
|
385
398
|
- "app/views/layouts/**/*"
|
|
386
|
-
- "app/controllers/application_controller.rb"
|
|
387
|
-
|
|
388
|
-
DevDoc/I18n/RequireTranslation:
|
|
389
|
-
Description: "Localize user-facing strings in glib JSON-UI props; pass `t(...)` instead of a hardcoded string."
|
|
390
|
-
Enabled: false
|
|
391
|
-
# WatchedMethods: glib component builder methods whose hash props carry
|
|
392
|
-
# user-facing text. LocalizableKeys: the prop names checked on those calls.
|
|
393
|
-
# Extend both if your project adds custom components or text props.
|
|
394
|
-
WatchedMethods:
|
|
395
|
-
- h1
|
|
396
|
-
- h2
|
|
397
|
-
- h3
|
|
398
|
-
- h4
|
|
399
|
-
- h5
|
|
400
|
-
- p
|
|
401
|
-
- label
|
|
402
|
-
- markdown
|
|
403
|
-
- fields_text
|
|
404
|
-
- fields_number
|
|
405
|
-
- fields_select
|
|
406
|
-
- fields_password
|
|
407
|
-
- fields_textarea
|
|
408
|
-
- fields_check
|
|
409
|
-
- fields_checkGroup
|
|
410
|
-
- fields_chipGroup
|
|
411
|
-
- fields_timeZone
|
|
412
|
-
- fields_radioGroup
|
|
413
|
-
- fields_date
|
|
414
|
-
- fields_datetime
|
|
415
|
-
LocalizableKeys:
|
|
416
|
-
- title
|
|
417
|
-
- subtitle
|
|
418
|
-
- subsubtitle
|
|
419
|
-
- label
|
|
420
|
-
- placeholder
|
|
421
|
-
- text
|
|
422
|
-
Include:
|
|
423
|
-
- "app/views/**/*.jbuilder"
|
|
424
|
-
- "app/views/**/*.rb"
|
|
425
|
-
|
|
426
|
-
DevDoc/I18n/TranslationKeyPrefix:
|
|
427
|
-
Description: "Translation keys must start with an allowed namespace prefix (e.g. `hotel.`, `general.`)."
|
|
428
|
-
Enabled: false
|
|
429
|
-
# AllowedPrefixes is project-specific, so it defaults to empty and the cop is
|
|
430
|
-
# a no-op until configured. Matches `t`, `translate`, and `I18n.t`. Only
|
|
431
|
-
# statically-literal keys are checked; dynamic/interpolated keys are skipped.
|
|
432
|
-
AllowedPrefixes: []
|
|
433
|
-
Include:
|
|
434
|
-
- "app/views/**/*.jbuilder"
|
|
435
|
-
- "app/views/**/*.rb"
|
|
436
|
-
- "app/controllers/**/*.rb"
|
|
437
|
-
- "app/mailers/**/*.rb"
|
|
438
|
-
- "app/helpers/**/*.rb"
|
|
439
|
-
|
|
440
|
-
DevDoc/I18n/UnverifiedTranslation:
|
|
441
|
-
Description: "Warn when a glib text prop is set from a non-literal value that isn't a `t(...)` call — it may be an unlocalized string."
|
|
442
|
-
# A review aid, not a clean lint: it can't tell a stashed literal from
|
|
443
|
-
# dynamic data (`user.name`) or a translation reached via a variable. Off by
|
|
444
|
-
# default and `info` severity — run it during a localization pass. Mirrors
|
|
445
|
-
# RequireTranslation's WatchedMethods/LocalizableKeys.
|
|
446
|
-
Enabled: false
|
|
447
|
-
Severity: info
|
|
448
|
-
WatchedMethods:
|
|
449
|
-
- h1
|
|
450
|
-
- h2
|
|
451
|
-
- h3
|
|
452
|
-
- h4
|
|
453
|
-
- h5
|
|
454
|
-
- p
|
|
455
|
-
- label
|
|
456
|
-
- markdown
|
|
457
|
-
- fields_text
|
|
458
|
-
- fields_number
|
|
459
|
-
- fields_select
|
|
460
|
-
- fields_password
|
|
461
|
-
- fields_textarea
|
|
462
|
-
- fields_check
|
|
463
|
-
- fields_checkGroup
|
|
464
|
-
- fields_chipGroup
|
|
465
|
-
- fields_timeZone
|
|
466
|
-
- fields_radioGroup
|
|
467
|
-
- fields_date
|
|
468
|
-
- fields_datetime
|
|
469
|
-
LocalizableKeys:
|
|
470
|
-
- title
|
|
471
|
-
- subtitle
|
|
472
|
-
- subsubtitle
|
|
473
|
-
- label
|
|
474
|
-
- placeholder
|
|
475
|
-
- text
|
|
476
|
-
Include:
|
|
477
|
-
- "app/views/**/*.jbuilder"
|
|
478
|
-
- "app/views/**/*.rb"
|
|
479
|
-
|
|
480
|
-
DevDoc/I18n/ReportText:
|
|
481
|
-
Description: "Report every user-facing glib text prop — hardcoded and already-localized — to collect all possible texts."
|
|
482
|
-
# A tooling aid, not a lint: unlike RequireTranslation it fires on *every*
|
|
483
|
-
# text value (including `t(...)` calls) so you can sweep the codebase and
|
|
484
|
-
# collect the full list of user-facing strings. Off by default and `info`
|
|
485
|
-
# severity. Mirrors RequireTranslation's WatchedMethods/LocalizableKeys.
|
|
486
|
-
Enabled: false
|
|
487
|
-
Severity: info
|
|
488
|
-
WatchedMethods:
|
|
489
|
-
- h1
|
|
490
|
-
- h2
|
|
491
|
-
- h3
|
|
492
|
-
- h4
|
|
493
|
-
- h5
|
|
494
|
-
- p
|
|
495
|
-
- label
|
|
496
|
-
- markdown
|
|
497
|
-
- fields_text
|
|
498
|
-
- fields_number
|
|
499
|
-
- fields_select
|
|
500
|
-
- fields_password
|
|
501
|
-
- fields_textarea
|
|
502
|
-
- fields_check
|
|
503
|
-
- fields_checkGroup
|
|
504
|
-
- fields_chipGroup
|
|
505
|
-
- fields_timeZone
|
|
506
|
-
- fields_radioGroup
|
|
507
|
-
- fields_date
|
|
508
|
-
- fields_datetime
|
|
509
|
-
LocalizableKeys:
|
|
510
|
-
- title
|
|
511
|
-
- subtitle
|
|
512
|
-
- subsubtitle
|
|
513
|
-
- label
|
|
514
|
-
- placeholder
|
|
515
|
-
- text
|
|
516
|
-
Include:
|
|
517
|
-
- "app/views/**/*.jbuilder"
|
|
518
|
-
- "app/views/**/*.rb"
|
|
@@ -56,13 +56,56 @@ module RuboCop
|
|
|
56
56
|
#
|
|
57
57
|
# ## Allowed paths (Exclude:)
|
|
58
58
|
# By default the cop is silent in:
|
|
59
|
-
# app/policies/**/*.rb
|
|
59
|
+
# app/policies/**/*.rb ← auth-dependent checks belong here
|
|
60
60
|
# app/helpers/**/*.rb
|
|
61
61
|
# app/controllers/concerns/**/*.rb
|
|
62
|
-
# app/views/layouts/**/*
|
|
63
|
-
# app/controllers/application_controller.rb
|
|
62
|
+
# app/views/layouts/**/* ← display branching (nav bars, etc.)
|
|
64
63
|
#
|
|
65
|
-
# Override via `Exclude:` in your `.rubocop.yml`.
|
|
64
|
+
# Override via `Exclude:` in your `.rubocop.yml`. Note: literal file
|
|
65
|
+
# paths (e.g. `app/controllers/application_controller.rb`) in
|
|
66
|
+
# `.rubocop.yml` `Exclude:` lists are flagged by the
|
|
67
|
+
# `DevDoc::Test::Lints::NoFileExcludes` lint — they hide the
|
|
68
|
+
# suppression from readers of that file. If `ApplicationController`
|
|
69
|
+
# needs an exception, use an inline `# rubocop:disable` at the
|
|
70
|
+
# specific line with a reason.
|
|
71
|
+
#
|
|
72
|
+
# ## Before disabling inline, consider the Policy
|
|
73
|
+
# The Policy exclusion is not accidental — it's the canonical
|
|
74
|
+
# Rails home for auth-dependent branching. If the flagged line
|
|
75
|
+
# is an authorization check (`return if @record.accessible_by?(current_user)`,
|
|
76
|
+
# `if current_user.admin?`, etc.), the right move is almost
|
|
77
|
+
# always to push that check into a Pundit policy method, not
|
|
78
|
+
# disable inline. The controller becomes
|
|
79
|
+
# `return unless policy(@record).access?` — same behaviour,
|
|
80
|
+
# no `current_user` reference in the controller, and the auth
|
|
81
|
+
# logic is reusable + testable in isolation.
|
|
82
|
+
#
|
|
83
|
+
# Even better: declare `glib_authorize_resource` at the
|
|
84
|
+
# controller level. glib-web runs the appropriate policy
|
|
85
|
+
# method before each action automatically, so the per-action
|
|
86
|
+
# `verify_access` / `authorize @record` line is usually not
|
|
87
|
+
# needed at all — the controller body no longer references
|
|
88
|
+
# `current_user` because the auth check has moved out of the
|
|
89
|
+
# action entirely. See `best_practices/backend/en/05_controller.md`
|
|
90
|
+
# item #7 for the canonical pattern.
|
|
91
|
+
#
|
|
92
|
+
# ❌ Auth check in the controller — flagged
|
|
93
|
+
# def verify_access
|
|
94
|
+
# return if @support_question.accessible_by?(current_user)
|
|
95
|
+
# # ... token fallback ...
|
|
96
|
+
# end
|
|
97
|
+
#
|
|
98
|
+
# ✔️ Same check in the Policy — silently allowed
|
|
99
|
+
# # app/policies/support_question_policy.rb
|
|
100
|
+
# def access?
|
|
101
|
+
# user.present? && record.accessible_by?(user)
|
|
102
|
+
# end
|
|
103
|
+
#
|
|
104
|
+
# # in the controller:
|
|
105
|
+
# def verify_access
|
|
106
|
+
# return if policy(@support_question).access?
|
|
107
|
+
# # ... token fallback ...
|
|
108
|
+
# end
|
|
66
109
|
#
|
|
67
110
|
# NOTE: The cop does not autocorrect — there is no mechanical fix. The
|
|
68
111
|
# right response depends on developer intent: drop the branch (if auth
|
|
@@ -43,6 +43,13 @@ module RuboCop
|
|
|
43
43
|
# end
|
|
44
44
|
# end
|
|
45
45
|
#
|
|
46
|
+
# ## Forms covered
|
|
47
|
+
# The cop catches `default:` set at column-creation time AND `default:`
|
|
48
|
+
# set later via `change_column_default(..., to: <non-nil>)`. Both are
|
|
49
|
+
# the same anti-pattern — a permanent default living in the schema.
|
|
50
|
+
# `change_column_default(..., to: nil)` (removing a default) is not
|
|
51
|
+
# flagged; that is the cleanup form.
|
|
52
|
+
#
|
|
46
53
|
# ## Exception (auto-detected)
|
|
47
54
|
# For performance reasons (large tables with millions of records) or when
|
|
48
55
|
# using `null: false`, you may temporarily set a default and then
|
|
@@ -51,6 +58,10 @@ module RuboCop
|
|
|
51
58
|
# for the same table and column anywhere in the same method body (`def
|
|
52
59
|
# change` / `def up`), including inside `reversible do |dir| dir.up`.
|
|
53
60
|
#
|
|
61
|
+
# The exception applies symmetrically:
|
|
62
|
+
# - `add_column ... default: X` paired with `change_column_default ... to: nil` → no offense
|
|
63
|
+
# - `change_column_default ... to: X` paired with `change_column_default ... to: nil` → no offense
|
|
64
|
+
#
|
|
54
65
|
# ✔ (no offense — two-step pattern auto-detected)
|
|
55
66
|
# add_column :users, :profile_completion_rate, :float, default: 0.0
|
|
56
67
|
# change_column_default :users, :profile_completion_rate, from: 0.0, to: nil
|
|
@@ -62,14 +73,22 @@ module RuboCop
|
|
|
62
73
|
# # bad
|
|
63
74
|
# t.string :status, default: 'active'
|
|
64
75
|
#
|
|
76
|
+
# # bad — permanent default set via change_column_default
|
|
77
|
+
# change_column_default :users, :score, from: nil, to: 0
|
|
78
|
+
#
|
|
65
79
|
# # good
|
|
66
80
|
# add_column :users, :score, :integer
|
|
67
81
|
#
|
|
68
82
|
# # good (temporary default immediately removed — two-step pattern)
|
|
69
83
|
# add_column :users, :score, :integer, default: 0
|
|
70
84
|
# change_column_default :users, :score, from: 0, to: nil
|
|
85
|
+
#
|
|
86
|
+
# # good (removing an existing default)
|
|
87
|
+
# change_column_default :users, :score, from: 0, to: nil
|
|
71
88
|
class AvoidColumnDefault < Base
|
|
72
89
|
MSG = 'Avoid setting `default:` in migrations. Keep business logic defaults in the application layer.'.freeze
|
|
90
|
+
MSG_CHANGE = 'Avoid setting a non-nil default via `change_column_default`. ' \
|
|
91
|
+
'Keep business logic defaults in the application layer.'.freeze
|
|
73
92
|
|
|
74
93
|
COLUMN_METHODS = %i[
|
|
75
94
|
string integer float boolean datetime date text binary decimal
|
|
@@ -84,9 +103,22 @@ module RuboCop
|
|
|
84
103
|
(send _ :change_column_default (sym $_) (sym $_) (hash <(pair (sym :to) (nil)) ...>))
|
|
85
104
|
PATTERN
|
|
86
105
|
|
|
106
|
+
# change_column_default(:table, :col, ..., to: <non-nil>, ...) — captures table + col.
|
|
107
|
+
def_node_matcher :change_column_default_to_non_nil?, <<~PATTERN
|
|
108
|
+
(send _ :change_column_default (sym $_) (sym $_) (hash <(pair (sym :to) !nil) ...>))
|
|
109
|
+
PATTERN
|
|
110
|
+
|
|
87
111
|
def on_send(node)
|
|
88
|
-
|
|
112
|
+
if node.method?(:change_column_default)
|
|
113
|
+
check_change_column_default(node)
|
|
114
|
+
elsif column_method?(node)
|
|
115
|
+
check_column_method(node)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
89
120
|
|
|
121
|
+
def check_column_method(node)
|
|
90
122
|
options = node.arguments.find(&:hash_type?)
|
|
91
123
|
return unless options && has_default_option?(options)
|
|
92
124
|
return if two_step_pattern?(node)
|
|
@@ -94,7 +126,16 @@ module RuboCop
|
|
|
94
126
|
add_offense(find_default_pair(options))
|
|
95
127
|
end
|
|
96
128
|
|
|
97
|
-
|
|
129
|
+
def check_change_column_default(node)
|
|
130
|
+
captures = change_column_default_to_non_nil?(node)
|
|
131
|
+
return unless captures
|
|
132
|
+
|
|
133
|
+
table_name, col_name = captures
|
|
134
|
+
return if cancelled_in_same_method?(node, table_name, col_name)
|
|
135
|
+
|
|
136
|
+
options = node.arguments.find(&:hash_type?)
|
|
137
|
+
add_offense(find_to_pair(options), message: MSG_CHANGE)
|
|
138
|
+
end
|
|
98
139
|
|
|
99
140
|
def column_method?(node)
|
|
100
141
|
node.method?(:add_column) || COLUMN_METHODS.include?(node.method_name)
|
|
@@ -104,10 +145,20 @@ module RuboCop
|
|
|
104
145
|
options.pairs.find { |p| p.key.sym_type? && p.key.value == :default }
|
|
105
146
|
end
|
|
106
147
|
|
|
148
|
+
def find_to_pair(options)
|
|
149
|
+
options.pairs.find { |p| p.key.sym_type? && p.key.value == :to }
|
|
150
|
+
end
|
|
151
|
+
|
|
107
152
|
def two_step_pattern?(node)
|
|
108
153
|
table_name, col_name = extract_table_and_column(node)
|
|
109
154
|
return false unless table_name && col_name
|
|
110
155
|
|
|
156
|
+
cancelled_in_same_method?(node, table_name, col_name)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Is there a `change_column_default(..., to: nil)` for the same
|
|
160
|
+
# (table, col) anywhere in the enclosing method? Used by both forms.
|
|
161
|
+
def cancelled_in_same_method?(node, table_name, col_name)
|
|
111
162
|
enclosing_def = node.each_ancestor(:def).first
|
|
112
163
|
return false unless enclosing_def
|
|
113
164
|
|
|
@@ -2,20 +2,24 @@ module RuboCop
|
|
|
2
2
|
module Cop
|
|
3
3
|
module DevDoc
|
|
4
4
|
module Migration
|
|
5
|
-
# Avoid vague column names like `status` or `
|
|
5
|
+
# Avoid vague column names like `status`, `group`, or `kind`.
|
|
6
6
|
#
|
|
7
7
|
# ## Rationale
|
|
8
8
|
# Use more specific names that describe the domain context. A column
|
|
9
9
|
# named `status` reveals nothing about *what* status it tracks; a column
|
|
10
10
|
# named `processing_status` makes the intent obvious at the call site.
|
|
11
|
+
# `kind` is the same shape — a content-free "what variety" label that
|
|
12
|
+
# reads no better than `type` (which Rails reserves for STI).
|
|
11
13
|
#
|
|
12
14
|
# ❌
|
|
13
15
|
# t.string :status
|
|
14
16
|
# add_column :orders, :group, :integer
|
|
17
|
+
# t.integer :kind
|
|
15
18
|
#
|
|
16
19
|
# ✔️
|
|
17
20
|
# t.string :processing_status
|
|
18
21
|
# add_column :orders, :user_group, :integer
|
|
22
|
+
# t.integer :membership_kind
|
|
19
23
|
#
|
|
20
24
|
# ## Note about `type`
|
|
21
25
|
# `type` is reserved by Rails for Single Table Inheritance (STI). Even
|
|
@@ -29,10 +33,12 @@ module RuboCop
|
|
|
29
33
|
# # bad
|
|
30
34
|
# t.string :status
|
|
31
35
|
# add_column :orders, :group, :integer
|
|
36
|
+
# t.integer :kind
|
|
32
37
|
#
|
|
33
38
|
# # good
|
|
34
39
|
# t.string :processing_status
|
|
35
40
|
# add_column :orders, :user_group, :integer
|
|
41
|
+
# t.integer :membership_kind
|
|
36
42
|
class AvoidVagueColumnNames < Base
|
|
37
43
|
MSG = 'Avoid vague column name `%<name>s`. Use a more specific name that includes context.'.freeze
|
|
38
44
|
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module DevDoc
|
|
4
|
+
module Rails
|
|
5
|
+
# Avoid ordering by `id` as the primary (sole or leftmost) sort.
|
|
6
|
+
#
|
|
7
|
+
# ## Rationale
|
|
8
|
+
# `order(id: ...)` / `order(:id)` sorts by the primary key, which
|
|
9
|
+
# reflects insertion order (and, for UUIDs, random order) rather
|
|
10
|
+
# than a column the reader can reason about. It makes a list read
|
|
11
|
+
# as "most recent" by accident, hides the real ordering intent,
|
|
12
|
+
# and silently breaks the moment rows are imported out of sequence
|
|
13
|
+
# or the primary key type changes.
|
|
14
|
+
#
|
|
15
|
+
# Order by a meaningful business column instead, and reach for
|
|
16
|
+
# `id` only as a **secondary** key to break ties deterministically:
|
|
17
|
+
#
|
|
18
|
+
# ❌ Primary key is not a sort the reader can reason about
|
|
19
|
+
# Snapshot.order(id: :desc)
|
|
20
|
+
#
|
|
21
|
+
# ✔️ Business column carries the intent
|
|
22
|
+
# Snapshot.order(created_at: :desc)
|
|
23
|
+
#
|
|
24
|
+
# ✔️ `id` as a deterministic tiebreaker — never the primary sort
|
|
25
|
+
# Snapshot.order(created_at: :desc, id: :desc)
|
|
26
|
+
#
|
|
27
|
+
# See `best_practices/backend/en/05_controller.md` — always `.order()`
|
|
28
|
+
# when displaying more than one record, and reserve `order(id: ...)`
|
|
29
|
+
# for the rare case where id-ordering is genuinely intended.
|
|
30
|
+
#
|
|
31
|
+
# ## Scope
|
|
32
|
+
# Flags `order` and `reorder` where `:id` is the **sole** ordering
|
|
33
|
+
# column or the **leftmost** (primary) one, in hash form
|
|
34
|
+
# (`order(id: :desc)`), symbol form (`order(:id)`,
|
|
35
|
+
# `order(:id, :name)`), or array form (`order([:id])`).
|
|
36
|
+
#
|
|
37
|
+
# Only the FIRST argument is inspected — it is the primary sort;
|
|
38
|
+
# every later argument is a tiebreaker. Raw-SQL string ordering
|
|
39
|
+
# (`order("id")`, `order("id DESC")`) is the domain of
|
|
40
|
+
# `DevDoc/Rails/AvoidRawSql`, so this cop defers the moment its
|
|
41
|
+
# leading argument is a string, an `Arel.sql(...)`, or anything
|
|
42
|
+
# other than a hash/symbol/array — the primary sort can't be read
|
|
43
|
+
# statically, and flagging would risk a false positive on a
|
|
44
|
+
# tiebreaker.
|
|
45
|
+
#
|
|
46
|
+
# NOTE: Each `order`/`reorder` call is inspected in isolation, so a
|
|
47
|
+
# chained form like `rel.order(:created_at).order(id: :desc)` is
|
|
48
|
+
# flagged even though Rails *appends* chained `order` calls — making
|
|
49
|
+
# `id` a secondary tiebreaker there. The deterministic-tiebreaker
|
|
50
|
+
# idiom this cop endorses is the single-call hash/symbol form
|
|
51
|
+
# (`order(created_at: :desc, id: :desc)`); prefer that, or disable
|
|
52
|
+
# inline with a reason when the chained form is genuinely intended.
|
|
53
|
+
#
|
|
54
|
+
# ## `id` as a secondary key is allowed
|
|
55
|
+
# `order(created_at: :desc, id: :desc)` is the standard pattern for
|
|
56
|
+
# deterministic ordering — `id` only breaks ties after the business
|
|
57
|
+
# column. The cop allows `:id` in any non-primary position:
|
|
58
|
+
#
|
|
59
|
+
# ✔️ `id` breaks ties only — primary sort is `created_at`
|
|
60
|
+
# Snapshot.order(created_at: :desc, id: :desc)
|
|
61
|
+
# Snapshot.order(:created_at, :id)
|
|
62
|
+
#
|
|
63
|
+
# ## Exception
|
|
64
|
+
# When `id` is genuinely the intended primary sort (rare — usually a
|
|
65
|
+
# sign something is special about the code), suppress the offense
|
|
66
|
+
# with an inline disable comment and a brief reason — e.g. an
|
|
67
|
+
# append-only audit log where `id` is creation order, so
|
|
68
|
+
# id-ordering is the real intent rather than an accident.
|
|
69
|
+
#
|
|
70
|
+
# NOTE: A named scope or model method that wraps the offense hides
|
|
71
|
+
# it at the call site — `scope :newest, -> { order(id: :desc) }` is
|
|
72
|
+
# flagged in the model file, but callers of `.newest` are not. The
|
|
73
|
+
# cop flags the definition, which is where the decision belongs.
|
|
74
|
+
#
|
|
75
|
+
# NOTE: Prefer `order(created_at: :desc)` for deterministic
|
|
76
|
+
# results — see `best_practices/backend/en/05_controller.md` #4.
|
|
77
|
+
# The rare legitimate `order(id:)` cases (see Exception above) —
|
|
78
|
+
# e.g. a test grabbing a record a controller just created, where
|
|
79
|
+
# `created_at` ties across one request and `id` is the only
|
|
80
|
+
# reliable insertion-order proxy — take an inline disable with a
|
|
81
|
+
# reason, not a blanket `Exclude` of `spec/**`/`test/**` (that
|
|
82
|
+
# silences accidental id-ordering in tests).
|
|
83
|
+
#
|
|
84
|
+
# @example
|
|
85
|
+
# # bad — `id` is the sole or primary sort
|
|
86
|
+
# Snapshot.order(id: :desc)
|
|
87
|
+
# Snapshot.order(:id)
|
|
88
|
+
# Snapshot.order(id: :desc, created_at: :desc)
|
|
89
|
+
# Snapshot.order(:id, :created_at)
|
|
90
|
+
# submission.snapshots.reorder(id: :asc)
|
|
91
|
+
#
|
|
92
|
+
# # good — business column primary, `id` as a tiebreaker
|
|
93
|
+
# Snapshot.order(created_at: :desc)
|
|
94
|
+
# Snapshot.order(created_at: :desc, id: :desc)
|
|
95
|
+
# Snapshot.order(:created_at, :id)
|
|
96
|
+
class AvoidOrderingById < Base
|
|
97
|
+
MSG = 'Ordering by `id` as the primary sort exposes the primary ' \
|
|
98
|
+
"key's insertion order rather than a meaningful column. " \
|
|
99
|
+
'Order by a business column (e.g. `created_at:`), or move ' \
|
|
100
|
+
'`id` to a secondary position as a deterministic tiebreaker. ' \
|
|
101
|
+
'Disable with a reason when `id` is genuinely the intended ' \
|
|
102
|
+
'primary sort.'.freeze
|
|
103
|
+
|
|
104
|
+
RESTRICT_ON_SEND = %i[order reorder].freeze
|
|
105
|
+
|
|
106
|
+
def on_send(node)
|
|
107
|
+
first = first_ordering_column(node)
|
|
108
|
+
return unless first
|
|
109
|
+
return unless id_column?(first)
|
|
110
|
+
|
|
111
|
+
add_offense(node.loc.selector)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Safe navigation (`rel&.order(id: :desc)`) parses as a `:csend`
|
|
115
|
+
# node, which `on_send` does not visit — alias it so the same
|
|
116
|
+
# check covers both dispatch forms.
|
|
117
|
+
alias on_csend on_send
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
# The leftmost ordering column, or nil when the primary sort
|
|
122
|
+
# isn't statically knowable. Only the FIRST argument is inspected
|
|
123
|
+
# — it alone is the primary sort; every later argument is a
|
|
124
|
+
# tiebreaker.
|
|
125
|
+
#
|
|
126
|
+
# order(:created_at, id: :desc) → :created_at (id secondary)
|
|
127
|
+
# order(id: :desc) → :id pair key
|
|
128
|
+
# order([:id]) → :id (array recurses)
|
|
129
|
+
#
|
|
130
|
+
# Returns nil (defers) for a raw-SQL string (`DevDoc/Rails/
|
|
131
|
+
# AvoidRawSql` owns it) AND for any other non-structured leading
|
|
132
|
+
# arg — an `Arel.sql` node, a helper call, a splat, a literal —
|
|
133
|
+
# because the primary sort can't be read statically and flagging
|
|
134
|
+
# would risk a false positive on what may be a tiebreaker.
|
|
135
|
+
def first_ordering_column(node)
|
|
136
|
+
ordering_column_of(node.arguments.first)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Resolve the primary sort from a single argument node, recursing
|
|
140
|
+
# into an array (`order([:id])`) so the array form matches the
|
|
141
|
+
# bare-symbol form.
|
|
142
|
+
def ordering_column_of(arg)
|
|
143
|
+
return nil unless arg
|
|
144
|
+
return nil if arg.str_type? || arg.dstr_type?
|
|
145
|
+
return first_hash_key(arg) if arg.hash_type?
|
|
146
|
+
return ordering_column_of(arg.children.first) if arg.array_type?
|
|
147
|
+
return arg if arg.sym_type?
|
|
148
|
+
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def first_hash_key(hash_node)
|
|
153
|
+
pair = hash_node.pairs.first
|
|
154
|
+
return nil unless pair
|
|
155
|
+
|
|
156
|
+
key = pair.key
|
|
157
|
+
key if key&.sym_type?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def id_column?(node)
|
|
161
|
+
node.sym_type? && node.value == :id
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -22,12 +22,19 @@ module RuboCop
|
|
|
22
22
|
# class Order < ApplicationRecord
|
|
23
23
|
# def save_with_confirmation_email
|
|
24
24
|
# transaction do
|
|
25
|
-
# save
|
|
26
|
-
#
|
|
25
|
+
# saved = save
|
|
26
|
+
# invoice.save! if saved
|
|
27
27
|
# end
|
|
28
|
+
# OrderMailer.confirmation(self).deliver_later if saved
|
|
28
29
|
# end
|
|
29
30
|
# end
|
|
30
31
|
#
|
|
32
|
+
# The `deliver_later` is *outside* the transaction deliberately —
|
|
33
|
+
# `DevDoc/Rails/NoDeliverLaterInTransaction` flags mailers/jobs
|
|
34
|
+
# queued inside a transaction (the job may run before commit and
|
|
35
|
+
# read stale data). The `saved` local leaks out of the block
|
|
36
|
+
# (Ruby's normal scoping), gating the mailer on actual success.
|
|
37
|
+
#
|
|
31
38
|
# ## When you have multiple call sites needing the same guard
|
|
32
39
|
# The single-method-per-place pattern above works when there is one
|
|
33
40
|
# natural call site. When several controllers/jobs/bulk operations all
|
|
@@ -101,8 +108,11 @@ module RuboCop
|
|
|
101
108
|
#
|
|
102
109
|
# # good
|
|
103
110
|
# def save_with_confirmation
|
|
104
|
-
# transaction
|
|
105
|
-
#
|
|
111
|
+
# transaction do
|
|
112
|
+
# saved = save
|
|
113
|
+
# invoice.save! if saved
|
|
114
|
+
# end
|
|
115
|
+
# send_confirmation if saved
|
|
106
116
|
# end
|
|
107
117
|
class AvoidRailsCallbacks < Base
|
|
108
118
|
CALLBACKS = %i[
|