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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d919e99af15a6422fabc24cd8cd5697d45777275150060f5df47fd20be56f386
4
- data.tar.gz: 4998be254bdb93920dfa694ab035a588bb5b44f768fa9f1867814c7e88feed19
3
+ metadata.gz: f669dc6f7e91c7e3aaac4e42e680df95fc24acbf6b467e7831387866cc2dc0d4
4
+ data.tar.gz: 41f06d6d2b20efd95b60389aae5f6803d206ad8f844d8de9daf04d87369ebf8f
5
5
  SHA512:
6
- metadata.gz: 13dcfa0d80c10d12254a6fba6ddcbc05ec075650184739ff705ced68fc59a0743bdef285e8f990fb44bd027cc84c97db392b5fee3ba54e4b48ededd0d2c8711f
7
- data.tar.gz: b8bf463155762956e6c2ed5619554f2a84a70765c9619a94fb06d3190f056d29c8230bbff1f11e4ccca8560cab3765106f9f1b4f7abe9da68a9760e1e7f236bc
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"
@@ -1,7 +1,7 @@
1
1
  module RuboCop
2
2
  module Cop
3
3
  module DevDoc
4
- module Migration
4
+ module Rails
5
5
  # Avoid methods that bypass validations and callbacks.
6
6
  #
7
7
  # ## Rationale
@@ -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/Migration/AvoidBypassingValidation` (they
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/Migration/AvoidBypassingValidation`
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/Migration/AvoidBypassingValidation`
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
- # To keep false positives low, the cop skips receivers that clearly
33
- # aren't AR relations: array literals (`[...]`), hash literals
34
- # (`{...}`), screaming-case constants (e.g. `PRICING_PLANS`), and
35
- # send-chains ending in a method known to return a non-Relation:
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
- # `AdditionalNonRelationMethods` (default `[]`): per-project list of
67
- # method names that return non-Relation collections. Useful when a
68
- # codebase has its own helper methods returning plain Arrays / Hashes
69
- # (e.g. a presenter factory) — adding them here lets the cop skip
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
- # AdditionalNonRelationMethods:
74
- # - all_items
75
- # - for_account
76
- # - gst_registration_ranges
77
- #
78
- # NOTE: The cop cannot determine whether a local variable, instance
79
- # variable, or method parameter is an AR relation or a plain
80
- # collection. When the receiver is in fact a plain Array/Hash, add
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
- to_a to_ary
118
- values keys
119
- map flat_map collect collect_concat filter_map
120
- flatten compact uniq sort sort_by reverse
121
- reduce inject each_with_object
122
- split lines chars bytes
123
- zip take drop drop_while take_while
124
- group_by partition tally tally_by chunk_while slice_when
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
- def excluded_receiver?(receiver)
152
- return true if receiver.array_type? || receiver.hash_type?
153
- return true if receiver.const_type? && screaming_case_const?(receiver)
154
- return excluded_receiver?(receiver.children.first) if receiver.begin_type?
155
- return true if receiver.send_type? && non_relation_method?(receiver.method_name)
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
- # Merge built-in known-safe methods with project-specific ones from
161
- # `AdditionalNonRelationMethods` in .rubocop.yml. The config knob
162
- # lets each project opt-in their own domain methods (e.g. a
163
- # `CashBookEntry.for_account` that returns an Array of plain Ruby
164
- # presenters) without modifying this cop.
165
- def non_relation_method?(method_name)
166
- return true if NON_RELATION_RETURNING_METHODS.include?(method_name)
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
- additional = cop_config['AdditionalNonRelationMethods'] || []
169
- additional.map(&:to_sym).include?(method_name)
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 screaming_case_const?(const_node)
173
- const_node.short_name.to_s.match?(/\A[A-Z][A-Z0-9_]*\z/)
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
- # 2+ arg destructuring means the iterator yields a pair/tuple
186
- # (Hash#each, zip, etc.). AR relations only yield single records.
187
- return true if args.children.length >= 2
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
- # Single-arg block: inspect how the arg is used inside the body.
190
- return false unless args.children.length == 1
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
- body = block_node.body
197
- return false unless body
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
- block_arg_indicates_non_record?(body, arg_name)
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
- next unless send.method_name == :[]
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
- false
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
- # - A field on an UNBOUND form (pre-auth flows: sessions, password
33
- # reset) cannot use `prop:` there is no model to resolve against.
34
- # Prefer binding a fresh instance (e.g. `User.new`) so `prop:`
35
- # works; otherwise keep `name:` with a per-line
36
- # `rubocop:disable ... -- <reason>`.
37
- # - Virtual/transient/credential fields where model-driven resolution
38
- # is undesirable: per-line disable with a reason.
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/validation from i18n. After switching, remove ' \
69
- 'the now-redundant hardcoded `label:`/`placeholder:` (verify the i18n keys exist). ' \
70
- 'If this field deliberately posts a raw param, disable with a reason.'.freeze
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
@@ -1,5 +1,5 @@
1
1
  module RuboCop
2
2
  module DevDoc
3
- VERSION = "0.10.0".freeze
3
+ VERSION = "0.10.1".freeze
4
4
  end
5
5
  end
@@ -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.0
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-05 00:00:00.000000000 Z
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