rubocop-dev_doc 0.10.0 → 0.10.1
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 +18 -1
- data/lib/rubocop/cop/dev_doc/{migration → rails}/avoid_bypassing_validation.rb +1 -1
- data/lib/rubocop/cop/dev_doc/rails/avoid_raw_sql.rb +3 -3
- data/lib/rubocop/cop/dev_doc/rails/no_block_predicate_on_relation.rb +133 -78
- data/lib/rubocop/cop/dev_doc/test/no_persistence_in_mailer_previews.rb +16 -0
- data/lib/rubocop/cop/dev_doc/view/prefer_prop_over_name.rb +64 -11
- data/lib/rubocop/dev_doc/version.rb +1 -1
- data/lib/rubocop/dev_doc.rb +1 -0
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f669dc6f7e91c7e3aaac4e42e680df95fc24acbf6b467e7831387866cc2dc0d4
|
|
4
|
+
data.tar.gz: 41f06d6d2b20efd95b60389aae5f6803d206ad8f844d8de9daf04d87369ebf8f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7cdef60287b7ef58701aab3c5e025416b2033d93c3a09d06b6addc3636daaf169010584df484ec6377caa192d74d901ee52a0abf56473490c3f4e289cf6895ba
|
|
7
|
+
data.tar.gz: 84a55b7ffaf35e17e8ee436c7f696c176659c30394fa7ed6a50a7bc46fa64ecbb2b5728a5df6547d457ba0006154e9279409b7fc2f6075f4937d0a0406a37ee6
|
data/config/default.yml
CHANGED
|
@@ -96,7 +96,11 @@ DevDoc/Migration/BooleanColumnNotNull:
|
|
|
96
96
|
# Enabled: false
|
|
97
97
|
# Use AllowedMethods for skips that are by-design in your project (touch,
|
|
98
98
|
# counter caches) instead of per-site disables.
|
|
99
|
-
DevDoc/Migration/AvoidBypassingValidation
|
|
99
|
+
# Renamed from DevDoc/Migration/AvoidBypassingValidation in 0.9.0 — the cop was
|
|
100
|
+
# never migration-specific (deliberately global, no Include); the old department
|
|
101
|
+
# name misled readers into thinking app code was unguarded. Old name raises a
|
|
102
|
+
# rename error via config/obsoletion.yml.
|
|
103
|
+
DevDoc/Rails/AvoidBypassingValidation:
|
|
100
104
|
Description: "Avoid methods that bypass validations and callbacks (`update_column`, `update_all`, `insert_all`, `touch`, etc.). Superset of Rails/SkipsModelValidations — disable that cop when enabling this one."
|
|
101
105
|
Enabled: false
|
|
102
106
|
AllowedMethods: []
|
|
@@ -177,9 +181,16 @@ DevDoc/Rails/EnumColumnNotNull:
|
|
|
177
181
|
Include:
|
|
178
182
|
- "app/models/**/*.rb"
|
|
179
183
|
|
|
184
|
+
# Receiver detection is inclusion-based (precision-first): the cop only fires
|
|
185
|
+
# when the chain visibly proves a relation (where/joins/includes/order/... or
|
|
186
|
+
# a scope listed in AdditionalRelationMethods). Opaque receivers and bare
|
|
187
|
+
# association reads are NOT flagged — the prior exclusion-based design
|
|
188
|
+
# dogfooded at 21 offenses / 0 true positives on a mature codebase. Recover
|
|
189
|
+
# recall on hot association/scope names via AdditionalRelationMethods.
|
|
180
190
|
DevDoc/Rails/NoBlockPredicateOnRelation:
|
|
181
191
|
Description: "Avoid block-form `count`/`reject`/`select`/`find`/`any?` on AR relations; push the predicate into SQL with `.where(...)` or a scope."
|
|
182
192
|
Enabled: true
|
|
193
|
+
AdditionalRelationMethods: []
|
|
183
194
|
AdditionalNonRelationMethods: []
|
|
184
195
|
Exclude:
|
|
185
196
|
- "spec/**/*"
|
|
@@ -652,8 +663,14 @@ DevDoc/View/PreferPropOverName:
|
|
|
652
663
|
# sibling label:/placeholder: for manual removal.
|
|
653
664
|
Enabled: false
|
|
654
665
|
SafeAutoCorrect: false
|
|
666
|
+
# fields_creditCard and fields_dynamicGroup are structurally non-model:
|
|
667
|
+
# credit-card fields capture transient credentials that must never bind to
|
|
668
|
+
# a persisted model, and dynamic-group names are array-param structural keys
|
|
669
|
+
# with no label/placeholder to derive.
|
|
655
670
|
ExemptFieldMethods:
|
|
656
671
|
- fields_hidden
|
|
672
|
+
- fields_creditCard
|
|
673
|
+
- fields_dynamicGroup
|
|
657
674
|
Include:
|
|
658
675
|
- "app/views/**/*.jbuilder"
|
|
659
676
|
- "app/views/**/*.rb"
|
|
@@ -59,7 +59,7 @@ module RuboCop
|
|
|
59
59
|
# For **single records**, use `Model.increment_counter(:col, id)`
|
|
60
60
|
# or `Model.update_counters(id, col: 1)` — both generate
|
|
61
61
|
# parameterized SQL and surface typos in the column name. Neither
|
|
62
|
-
# is flagged by `DevDoc/
|
|
62
|
+
# is flagged by `DevDoc/Rails/AvoidBypassingValidation` (they
|
|
63
63
|
# are the Rails-blessed atomic-counter primitives).
|
|
64
64
|
#
|
|
65
65
|
# For **bulk counter updates**, the only clean option is
|
|
@@ -67,7 +67,7 @@ module RuboCop
|
|
|
67
67
|
# queries, slow on large tables, but free of the
|
|
68
68
|
# validation-bypass smell. If the N-query cost is genuinely
|
|
69
69
|
# unacceptable, `update_all("col = col + 1")` requires disabling
|
|
70
|
-
# BOTH this cop AND `DevDoc/
|
|
70
|
+
# BOTH this cop AND `DevDoc/Rails/AvoidBypassingValidation`
|
|
71
71
|
# with reasons — the friction is the audit trail (locking
|
|
72
72
|
# implications, idempotency on re-runs, and the like are worth
|
|
73
73
|
# a second look).
|
|
@@ -95,7 +95,7 @@ module RuboCop
|
|
|
95
95
|
# migration failing because a model changed.
|
|
96
96
|
#
|
|
97
97
|
# The correct backfill pattern goes through the model with
|
|
98
|
-
# `save!` — `DevDoc/
|
|
98
|
+
# `save!` — `DevDoc/Rails/AvoidBypassingValidation`
|
|
99
99
|
# enforces this, and `best_practices/backend/en/02_migration.md`
|
|
100
100
|
# shows the shape:
|
|
101
101
|
#
|
|
@@ -28,11 +28,31 @@ module RuboCop
|
|
|
28
28
|
# attributes, non-trivial Ruby logic). For those, add a
|
|
29
29
|
# `# rubocop:disable` with a brief reason.
|
|
30
30
|
#
|
|
31
|
+
# ## Receiver detection is precision-first (inclusion, not exclusion)
|
|
32
|
+
# The cop only fires when the receiver chain PROVES it is a relation:
|
|
33
|
+
# it must contain a relation-returning method (`where`, `joins`,
|
|
34
|
+
# `includes`, `order`, `limit`, ... — see RELATION_RETURNING_METHODS)
|
|
35
|
+
# or a project-declared scope from `AdditionalRelationMethods`.
|
|
36
|
+
# Opaque receivers (locals, ivars, method params, bare association
|
|
37
|
+
# reads like `user.posts`) are NOT flagged.
|
|
38
|
+
#
|
|
39
|
+
# This is a deliberate trade. The earlier exclusion-based design
|
|
40
|
+
# ("flag anything that isn't provably a non-relation") was dogfooded
|
|
41
|
+
# against a mature codebase and scored 21 offenses with ZERO true
|
|
42
|
+
# positives — params arrays, gem data, AST enumerators, and validators
|
|
43
|
+
# that must see unsaved in-memory records. A cop that is all noise
|
|
44
|
+
# teaches people to write disables, which is worse than the missed
|
|
45
|
+
# recall: a bare `user.posts.any? { }` slipping through is a perf
|
|
46
|
+
# nit; twenty reflex disables are a culture problem. Projects can
|
|
47
|
+
# recover recall selectively by listing their hot association/scope
|
|
48
|
+
# names in `AdditionalRelationMethods`.
|
|
49
|
+
#
|
|
31
50
|
# ## Excluded receivers
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
51
|
+
# Even within a proven relation chain, the cop skips receivers whose
|
|
52
|
+
# FINAL call is known to return a non-Relation (the collection is
|
|
53
|
+
# already materialised, so SQL push-down is no longer possible), plus
|
|
54
|
+
# obvious non-relations: array literals (`[...]`), hash literals
|
|
55
|
+
# (`{...}`), and screaming-case constants (e.g. `PRICING_PLANS`):
|
|
36
56
|
# * Array-returning — `pluck`, `to_a`, `map`, `flatten`, `compact`,
|
|
37
57
|
# `uniq`, `sort`, `sort_by`, `reduce`, `inject`, `each_with_object`,
|
|
38
58
|
# `zip`, `take`, `drop`, `group_by`, `partition`, `tally`,
|
|
@@ -63,24 +83,20 @@ module RuboCop
|
|
|
63
83
|
# proves the element is a String.
|
|
64
84
|
#
|
|
65
85
|
# ## Configuration
|
|
66
|
-
# `
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
# send-chains ending in those methods. Example:
|
|
86
|
+
# `AdditionalRelationMethods` (default `[]`): per-project list of
|
|
87
|
+
# association/scope names the cop should treat as relation-returning.
|
|
88
|
+
# This is how a project recovers recall on its hot paths without
|
|
89
|
+
# re-opening the false-positive door. Example:
|
|
71
90
|
#
|
|
72
91
|
# DevDoc/Rails/NoBlockPredicateOnRelation:
|
|
73
|
-
#
|
|
74
|
-
# -
|
|
75
|
-
# -
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
# `# rubocop:disable DevDoc/Rails/NoBlockPredicateOnRelation` with a
|
|
82
|
-
# brief reason — the friction is intentional and ensures the choice
|
|
83
|
-
# is reviewed.
|
|
92
|
+
# AdditionalRelationMethods:
|
|
93
|
+
# - accessible_organization_documents
|
|
94
|
+
# - active_subscriptions
|
|
95
|
+
#
|
|
96
|
+
# `AdditionalNonRelationMethods` (default `[]`): per-project list of
|
|
97
|
+
# method names that return non-Relation collections (e.g. a presenter
|
|
98
|
+
# factory) — a chain ENDING in one of these is skipped even when an
|
|
99
|
+
# earlier link is a relation method.
|
|
84
100
|
#
|
|
85
101
|
# @example
|
|
86
102
|
# # bad
|
|
@@ -108,31 +124,38 @@ module RuboCop
|
|
|
108
124
|
# out would make the alias a zero-cost dodge.
|
|
109
125
|
RESTRICT_ON_SEND = %i[count reject select filter find detect any?].freeze
|
|
110
126
|
|
|
127
|
+
# Methods returning an ActiveRecord::Relation — a chain containing one
|
|
128
|
+
# of these PROVES the receiver is a relation (inclusion-based
|
|
129
|
+
# detection; see the docstring). `merge`, `all` and `from` are
|
|
130
|
+
# deliberately absent: Hash#merge is everyday Ruby, `.all` is also
|
|
131
|
+
# the API of non-AR clients (e.g. Stripe list endpoints), and
|
|
132
|
+
# ActiveSupport adds Array#from.
|
|
133
|
+
RELATION_RETURNING_METHODS = %i[
|
|
134
|
+
where rewhere not joins left_joins left_outer_joins includes
|
|
135
|
+
preload eager_load references order reorder group having limit
|
|
136
|
+
offset distinct unscope unscoped lock readonly or
|
|
137
|
+
].freeze
|
|
138
|
+
|
|
111
139
|
# Methods whose return value is known to be a non-Relation collection
|
|
112
140
|
# (Array, Hash, or Enumerator). When a `.select`/`.reject`/etc. with a
|
|
113
141
|
# block is chained onto a call to one of these, the block runs over
|
|
114
142
|
# the materialised collection — pushing into SQL isn't possible.
|
|
115
143
|
NON_RELATION_RETURNING_METHODS = %i[
|
|
116
|
-
pluck pluck_to_hash
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
slice except merge transform_values transform_keys to_h
|
|
127
|
-
compact_blank with_indifferent_access index_by index_with
|
|
128
|
-
|
|
129
|
-
each_with_index each_slice each_cons each_entry each_key each_value each_pair
|
|
130
|
-
chunk slice_before slice_after lazy with_index with_object
|
|
144
|
+
pluck pluck_to_hash to_a to_ary values keys map flat_map collect
|
|
145
|
+
collect_concat filter_map flatten compact uniq sort sort_by reverse
|
|
146
|
+
reduce inject each_with_object split lines chars bytes zip take
|
|
147
|
+
drop drop_while take_while group_by partition tally tally_by
|
|
148
|
+
chunk_while slice_when slice except merge transform_values
|
|
149
|
+
transform_keys to_h compact_blank with_indifferent_access index_by
|
|
150
|
+
index_with each_with_index each_slice each_cons each_entry each_key
|
|
151
|
+
each_value each_pair chunk slice_before slice_after lazy with_index
|
|
152
|
+
with_object
|
|
131
153
|
].freeze
|
|
132
154
|
|
|
133
155
|
def on_send(node)
|
|
134
156
|
return unless node.block_literal? || symbol_block_pass?(node)
|
|
135
157
|
return if node.receiver.nil?
|
|
158
|
+
return unless relation_chain?(node.receiver)
|
|
136
159
|
return if excluded_receiver?(node.receiver)
|
|
137
160
|
return if excluded_block?(node)
|
|
138
161
|
|
|
@@ -148,55 +171,83 @@ module RuboCop
|
|
|
148
171
|
last&.block_pass_type? && last.children.first&.sym_type?
|
|
149
172
|
end
|
|
150
173
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
174
|
+
# Inclusion gate: walk down the receiver chain looking for a link
|
|
175
|
+
# that PROVES this is a relation. Opaque receivers (lvars, ivars,
|
|
176
|
+
# bare association reads) never pass — that's the precision-first
|
|
177
|
+
# doctrine documented above.
|
|
178
|
+
def relation_chain?(node)
|
|
179
|
+
current = node
|
|
180
|
+
while current
|
|
181
|
+
current = unwrap_begin(current)
|
|
182
|
+
break unless current&.send_type?
|
|
183
|
+
return true if relation_method?(current.method_name)
|
|
156
184
|
|
|
185
|
+
current = current.receiver
|
|
186
|
+
end
|
|
157
187
|
false
|
|
158
188
|
end
|
|
159
189
|
|
|
160
|
-
#
|
|
161
|
-
# `
|
|
162
|
-
#
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
190
|
+
# A paren group's value is its LAST child (`(a; b).select { }` runs
|
|
191
|
+
# the block on `b`), so unwrap to that — `children.first` would judge
|
|
192
|
+
# the discarded statement.
|
|
193
|
+
def unwrap_begin(node)
|
|
194
|
+
node = node.children.last while node&.begin_type?
|
|
195
|
+
node
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def relation_method?(method_name)
|
|
199
|
+
RELATION_RETURNING_METHODS.include?(method_name) ||
|
|
200
|
+
configured_methods('AdditionalRelationMethods').include?(method_name)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Even inside a proven relation chain, skip when the FINAL call
|
|
204
|
+
# materialises the collection (`where(...).pluck(:id)`,
|
|
205
|
+
# `where(...).to_a`, ...): the block then runs over an in-memory
|
|
206
|
+
# collection and SQL push-down is no longer possible.
|
|
207
|
+
def excluded_receiver?(receiver)
|
|
208
|
+
receiver = unwrap_begin(receiver)
|
|
209
|
+
receiver&.send_type? && non_relation_method?(receiver.method_name)
|
|
210
|
+
end
|
|
167
211
|
|
|
168
|
-
|
|
169
|
-
|
|
212
|
+
# Built-in known-safe methods plus project-specific ones from
|
|
213
|
+
# `AdditionalNonRelationMethods` (e.g. a `CashBookEntry.for_account`
|
|
214
|
+
# returning an Array of plain Ruby presenters).
|
|
215
|
+
def non_relation_method?(method_name)
|
|
216
|
+
NON_RELATION_RETURNING_METHODS.include?(method_name) ||
|
|
217
|
+
configured_methods('AdditionalNonRelationMethods').include?(method_name)
|
|
170
218
|
end
|
|
171
219
|
|
|
172
|
-
def
|
|
173
|
-
|
|
220
|
+
def configured_methods(key)
|
|
221
|
+
(cop_config[key] || []).map(&:to_sym)
|
|
174
222
|
end
|
|
175
223
|
|
|
176
224
|
# Look at the block parameters and body for evidence that the
|
|
177
|
-
# iterated element can't be an ActiveRecord record.
|
|
225
|
+
# iterated element can't be an ActiveRecord record. 2+ arg
|
|
226
|
+
# destructuring means the iterator yields a pair/tuple (Hash#each,
|
|
227
|
+
# zip, etc.) — AR relations only yield single records; for a
|
|
228
|
+
# single-arg block, inspect how the arg is used inside the body.
|
|
178
229
|
def excluded_block?(send_node)
|
|
179
230
|
block_node = send_node.block_node
|
|
180
|
-
return false unless block_node
|
|
181
|
-
|
|
182
|
-
args = block_node.arguments
|
|
183
|
-
return false unless args
|
|
231
|
+
return false unless block_node.respond_to?(:arguments)
|
|
232
|
+
return true if pair_destructuring_block?(block_node)
|
|
184
233
|
|
|
185
|
-
#
|
|
186
|
-
#
|
|
187
|
-
|
|
234
|
+
# `to_a` normalises across node kinds: a block's ArgsNode yields its
|
|
235
|
+
# children; a numblock's `arguments` is already a plain (empty) Array.
|
|
236
|
+
args = block_node.arguments.to_a
|
|
237
|
+
arg_name = args.length == 1 ? block_arg_name(args.first) : nil
|
|
238
|
+
return false unless arg_name && block_node.body
|
|
188
239
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
arg_node = args.children.first
|
|
193
|
-
arg_name = block_arg_name(arg_node)
|
|
194
|
-
return false unless arg_name
|
|
240
|
+
block_arg_indicates_non_record?(block_node.body, arg_name)
|
|
241
|
+
end
|
|
195
242
|
|
|
196
|
-
|
|
197
|
-
|
|
243
|
+
# 2+ block params — literal `|k, v|` or numbered (`_2` yields arity 2
|
|
244
|
+
# as an Integer child, there is no ArgsNode) — mean the iterator
|
|
245
|
+
# yields a pair/tuple (Hash#each, zip, ...); AR relations yield
|
|
246
|
+
# single records.
|
|
247
|
+
def pair_destructuring_block?(block_node)
|
|
248
|
+
return block_node.children[1] >= 2 if block_node.numblock_type?
|
|
198
249
|
|
|
199
|
-
|
|
250
|
+
block_node.arguments.to_a.length >= 2
|
|
200
251
|
end
|
|
201
252
|
|
|
202
253
|
# Extract the simple name of a block argument, regardless of whether
|
|
@@ -214,18 +265,22 @@ module RuboCop
|
|
|
214
265
|
# * Single-character string-literal indexing — `arg[0] == '+'` —
|
|
215
266
|
# implies String
|
|
216
267
|
def block_arg_indicates_non_record?(body, arg_name)
|
|
217
|
-
body.each_descendant(:send) do |send|
|
|
218
|
-
|
|
219
|
-
next unless send.receiver&.lvar_type?
|
|
220
|
-
next unless send.receiver.children.first == arg_name
|
|
221
|
-
next unless send.arguments.length == 1
|
|
222
|
-
|
|
223
|
-
key = send.first_argument
|
|
224
|
-
return true if key.sym_type?
|
|
225
|
-
return true if key.int_type? && compared_to_single_char_string?(send)
|
|
268
|
+
body.each_descendant(:send).any? do |send|
|
|
269
|
+
indexes_block_arg?(send, arg_name) && non_record_key?(send)
|
|
226
270
|
end
|
|
271
|
+
end
|
|
227
272
|
|
|
228
|
-
|
|
273
|
+
# `arg[...]` with exactly one index argument, on the named block arg.
|
|
274
|
+
def indexes_block_arg?(send, arg_name)
|
|
275
|
+
send.method_name == :[] &&
|
|
276
|
+
send.receiver&.lvar_type? &&
|
|
277
|
+
send.receiver.children.first == arg_name &&
|
|
278
|
+
send.arguments.length == 1
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def non_record_key?(send)
|
|
282
|
+
key = send.first_argument
|
|
283
|
+
key.sym_type? || (key.int_type? && compared_to_single_char_string?(send))
|
|
229
284
|
end
|
|
230
285
|
|
|
231
286
|
# `arg[0] == '+'` — the [] send is one side of a `==` or `!=`
|
|
@@ -21,6 +21,22 @@ module RuboCop
|
|
|
21
21
|
# silently rewriting the first published checklist's note in the dev DB
|
|
22
22
|
# on every preview view. In-memory assignment renders the same body.
|
|
23
23
|
#
|
|
24
|
+
# ## When in-memory assignment can't work: add a FIXTURE, don't write
|
|
25
|
+
# Some mailers re-query their records at render time (async-safe
|
|
26
|
+
# designs re-scope ids against live access rules), so an in-memory
|
|
27
|
+
# attribute never reaches the render and persistence looks
|
|
28
|
+
# "irreducible". It still isn't — the fix is data, not code: put the
|
|
29
|
+
# state the variant needs into a fixture (or seeded row) and have the
|
|
30
|
+
# preview *select* it instead of *creating* it. Real case: a
|
|
31
|
+
# "documents shared with you" note variant used
|
|
32
|
+
# `doc.update_column(:standing_note, ...)` because the mailer
|
|
33
|
+
# re-queries the accessible documents; the fix was a fixture document
|
|
34
|
+
# that carries the standing note, with the two preview variants
|
|
35
|
+
# choosing their `document_ids` via `where(standing_note: nil)` vs
|
|
36
|
+
# all ids. Fixtures make the variant deterministic for snapshot-tested
|
|
37
|
+
# previews AND leave the dev DB untouched — never reach for an on-the-fly
|
|
38
|
+
# write (or an Exclude for this cop) before trying a fixture.
|
|
39
|
+
#
|
|
24
40
|
# `AllowedMethods` exists for projects whose preview data setup
|
|
25
41
|
# genuinely must write (e.g. a sandboxed preview database) — prefer
|
|
26
42
|
# leaving it empty.
|
|
@@ -29,13 +29,61 @@ module RuboCop
|
|
|
29
29
|
# hidden fields have no label/placeholder, and they are the
|
|
30
30
|
# dominant legitimate `name:` users (echoed tokens, dynamic-group
|
|
31
31
|
# indexes, forced values).
|
|
32
|
-
# -
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
32
|
+
# - `fields_creditCard` is exempt by default: it captures transient
|
|
33
|
+
# card credentials (tokenised client-side, stripped from params
|
|
34
|
+
# server-side) that must never bind to a persisted model.
|
|
35
|
+
# - `fields_dynamicGroup` is exempt by default: its `name:` is the
|
|
36
|
+
# structural array-param key for the group's rows, not a labelled
|
|
37
|
+
# model attribute — the row template's inner fields are where
|
|
38
|
+
# `prop:` belongs.
|
|
39
|
+
# ## How to fix — the decision ladder
|
|
40
|
+
# Work down this ladder before even considering a disable. A full
|
|
41
|
+
# convert-or-justify sweep of a mature codebase (27 offenses) ended
|
|
42
|
+
# with ZERO disables — every "deliberately raw" case fell to one of
|
|
43
|
+
# these:
|
|
44
|
+
#
|
|
45
|
+
# 1. **Form bound + real attribute** → plain `prop:`.
|
|
46
|
+
# 2. **Form unbound** (pre-auth, filters) → bind a model. A blank
|
|
47
|
+
# record works for auth flows (`model: User.new`); for
|
|
48
|
+
# params-driven filters, a small presentation-only ActiveModel
|
|
49
|
+
# form object (the filtering can keep running off permitted
|
|
50
|
+
# params — the object exists only so fields derive names, labels
|
|
51
|
+
# and sticky values).
|
|
52
|
+
# 3. **Param namespace differs from the bound model on purpose**
|
|
53
|
+
# (server composes another record from `other_thing[...]` params)
|
|
54
|
+
# → bind a form object whose namespace IS the contract; override
|
|
55
|
+
# `model_name` if the class name doesn't match, and pass an
|
|
56
|
+
# explicit `url:` (POROs have no route for polymorphic
|
|
57
|
+
# derivation).
|
|
58
|
+
# 4. **Virtual param** (posted key isn't a column; controller applies
|
|
59
|
+
# it manually) → add a READER-ONLY virtual attribute to the model
|
|
60
|
+
# that derives the prefill (e.g. a `visibility` reader derived
|
|
61
|
+
# from an enum, a `notification_note` reader defaulting to the
|
|
62
|
+
# saved note). No writer → no mass-assignment surface; the
|
|
63
|
+
# controller's manual handling is unchanged.
|
|
64
|
+
# 5. **Label-less by design** → keep `prop:` and pass `label: ''`.
|
|
65
|
+
# The empty string is truthy, so it blocks the i18n label
|
|
66
|
+
# derivation — "it must not have a label" is NOT a reason for raw
|
|
67
|
+
# `name:`.
|
|
68
|
+
#
|
|
69
|
+
# ## Legitimately raw (don't convert)
|
|
70
|
+
# - Deliberately TOP-LEVEL params the controller reads un-namespaced:
|
|
71
|
+
# mode switches (`params[:mode]`), auth params (`otp_attempt`),
|
|
72
|
+
# signed link tokens, bare GET search terms (`q`), client-routing
|
|
73
|
+
# pass-throughs (`target_form_id`). `prop:` would namespace them
|
|
74
|
+
# under `param_key[...]` and break the read.
|
|
75
|
+
# - Client-side-only controls that never post: select-all master
|
|
76
|
+
# toggles, show/hide drivers referenced via `{ "var": ... }`.
|
|
77
|
+
# - Bulk selection arrays (`foo[ids][]` with per-row `checkValue:`).
|
|
78
|
+
# - Names fixed by an external protocol (`g-recaptcha-response`).
|
|
79
|
+
#
|
|
80
|
+
# ## Trade-off to be aware of
|
|
81
|
+
# `prop:` removes the literal wire name from the source: you can no
|
|
82
|
+
# longer grep `user[email]` and land in the view — the name is
|
|
83
|
+
# assembled at render time. In exchange names become predictable
|
|
84
|
+
# (always `param_key[attr]`) and attribute renames fail loudly at
|
|
85
|
+
# render (`field_assert_respond_to`) instead of silently posting a
|
|
86
|
+
# dead param.
|
|
39
87
|
#
|
|
40
88
|
# ## Autocorrect (unsafe)
|
|
41
89
|
# `name: 'model[attr]'` becomes `prop: :attr`. Two caveats make this
|
|
@@ -65,9 +113,14 @@ module RuboCop
|
|
|
65
113
|
extend AutoCorrector
|
|
66
114
|
|
|
67
115
|
MSG = 'Bind the model attribute with `prop: :%<attr>s` instead of `name: %<name>s` — ' \
|
|
68
|
-
'`prop:` resolves label/placeholder
|
|
69
|
-
'
|
|
70
|
-
'
|
|
116
|
+
'`prop:` derives the field name and resolves label/placeholder from i18n. ' \
|
|
117
|
+
'No bound model is not a blocker: bind a blank record or an ActiveModel form ' \
|
|
118
|
+
'object (override `model_name` when the param namespace is the contract). A ' \
|
|
119
|
+
'virtual param is not a blocker: add a reader-only attribute deriving the ' \
|
|
120
|
+
'prefill. Label-less by design is not a blocker: keep `prop:` and pass ' \
|
|
121
|
+
"`label: ''`. Raw `name:` is right only for deliberately top-level params the " \
|
|
122
|
+
'controller reads un-namespaced (mode switches, auth params, search terms) ' \
|
|
123
|
+
'and client-side-only controls.'.freeze
|
|
71
124
|
|
|
72
125
|
MODEL_ATTR_NAME = /\A\w+\[(\w+)\]\z/
|
|
73
126
|
|
|
@@ -100,7 +153,7 @@ module RuboCop
|
|
|
100
153
|
end
|
|
101
154
|
|
|
102
155
|
def exempt_field_methods
|
|
103
|
-
cop_config.fetch('ExemptFieldMethods', %w[fields_hidden])
|
|
156
|
+
cop_config.fetch('ExemptFieldMethods', %w[fields_hidden fields_creditCard fields_dynamicGroup])
|
|
104
157
|
end
|
|
105
158
|
end
|
|
106
159
|
end
|
data/lib/rubocop/dev_doc.rb
CHANGED
|
@@ -9,5 +9,6 @@ module RuboCop
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
RuboCop::ConfigLoader.ignore_parent_exclusion = true
|
|
12
|
+
RuboCop::ConfigObsoletion.files << RuboCop::DevDoc::PROJECT_ROOT.join("config", "obsoletion.yml").to_s
|
|
12
13
|
|
|
13
14
|
Dir[File.join(__dir__, "cop", "dev_doc", "**", "*.rb")].each { |f| require f }
|
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.10.
|
|
4
|
+
version: 0.10.1
|
|
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-
|
|
11
|
+
date: 2026-07-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -102,7 +102,6 @@ files:
|
|
|
102
102
|
- lib/rubocop/cop/dev_doc/i18n/translation_key_prefix.rb
|
|
103
103
|
- lib/rubocop/cop/dev_doc/migration/amount_column_in_cents.rb
|
|
104
104
|
- lib/rubocop/cop/dev_doc/migration/avoid_boolean_column.rb
|
|
105
|
-
- lib/rubocop/cop/dev_doc/migration/avoid_bypassing_validation.rb
|
|
106
105
|
- lib/rubocop/cop/dev_doc/migration/avoid_column_default.rb
|
|
107
106
|
- lib/rubocop/cop/dev_doc/migration/avoid_conditional_schema_changes.rb
|
|
108
107
|
- lib/rubocop/cop/dev_doc/migration/avoid_json_column.rb
|
|
@@ -115,6 +114,7 @@ files:
|
|
|
115
114
|
- lib/rubocop/cop/dev_doc/migration/require_primary_key.rb
|
|
116
115
|
- lib/rubocop/cop/dev_doc/migration/require_timestamps.rb
|
|
117
116
|
- lib/rubocop/cop/dev_doc/rails/application_record_transaction.rb
|
|
117
|
+
- lib/rubocop/cop/dev_doc/rails/avoid_bypassing_validation.rb
|
|
118
118
|
- lib/rubocop/cop/dev_doc/rails/avoid_lifecycle_method_override.rb
|
|
119
119
|
- lib/rubocop/cop/dev_doc/rails/avoid_ordering_by_id.rb
|
|
120
120
|
- lib/rubocop/cop/dev_doc/rails/avoid_rails_callbacks.rb
|