vouchington-tooling 0.6.2 → 0.7.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.
Files changed (50) hide show
  1. package/README.md +4 -0
  2. package/ast-grep/rules/backend-no-test-file-imports.yml +44 -0
  3. package/ast-grep/rules/force-dynamic-layout.yml +57 -0
  4. package/ast-grep/rules/force-dynamic-pages.yml +38 -0
  5. package/ast-grep/rules/no-dynamic-import-in-vitest-config.yml +30 -0
  6. package/ast-grep/rules/no-dynamic-server-components-tsx.yml +71 -0
  7. package/ast-grep/rules/no-dynamic-server-components.yml +71 -0
  8. package/ast-grep/rules/no-enable-indexscan-off-tsx.yml +33 -0
  9. package/ast-grep/rules/no-enable-indexscan-off.yml +41 -0
  10. package/ast-grep/rules/no-exports-in-tests-folder-tsx.yml +42 -0
  11. package/ast-grep/rules/no-exports-in-tests-folder.yml +43 -0
  12. package/ast-grep/rules/no-object-assign-new-error.yml +31 -0
  13. package/ast-grep/rules/no-runner-temp-nullish.yml +33 -0
  14. package/ast-grep/rules/no-self-comparison-assertion-tsx.yml +43 -0
  15. package/ast-grep/rules/no-self-comparison-assertion.yml +53 -0
  16. package/ast-grep/rules/no-sql-offset.yml +65 -0
  17. package/ast-grep/rules/no-sync-throw-assert-on-async-enqueue.yml +42 -0
  18. package/ast-grep/rules/no-tautological-literal-assertion-tsx.yml +31 -0
  19. package/ast-grep/rules/no-tautological-literal-assertion.yml +38 -0
  20. package/ast-grep/rules/no-test-max-lines-disable-tsx.yml +41 -0
  21. package/ast-grep/rules/no-test-max-lines-disable.yml +53 -0
  22. package/ast-grep/rules/no-three-sequential-awaits-tsx.yml +288 -0
  23. package/ast-grep/rules/no-three-sequential-awaits.yml +242 -0
  24. package/ast-grep/rules/nullable-options.yml +429 -0
  25. package/ast-grep/rules/pagination-no-exact-limit-has-next.yml +111 -0
  26. package/ast-grep/rules/playwright-no-bare-meta-press.yml +29 -0
  27. package/ast-grep/rules/storybook-no-webpack-final.yml +31 -0
  28. package/ast-grep/rules/web-class-component-needs-use-client.yml +175 -0
  29. package/ast-grep/rules/web-clickable-needs-pointer.yml +74 -0
  30. package/ast-grep/rules/web-dialog-content-description.yml +56 -0
  31. package/ast-grep/rules/web-icon-button-tooltip.yml +82 -0
  32. package/ast-grep/rules/web-no-inline-scrollbar-hide.yml +41 -0
  33. package/ast-grep/rules/web-no-window-open-navigation.yml +39 -0
  34. package/ast-grep/sgconfig.yml +9 -0
  35. package/dist/ast-grep-pack/index.d.mts +6 -0
  36. package/dist/ast-grep-pack/index.mjs +11 -0
  37. package/dist/cli/commands/ast-grep-pack.d.mts +2 -0
  38. package/dist/cli/commands/ast-grep-pack.mjs +11 -0
  39. package/dist/cli/index.mjs +3 -0
  40. package/dist/cli/parse-options.d.mts +1 -0
  41. package/dist/cli/parse-options.mjs +6 -0
  42. package/dist/cli/parse.d.mts +2 -0
  43. package/dist/cli/parse.mjs +3 -1
  44. package/dist/cli/usage.d.mts +1 -1
  45. package/dist/cli/usage.mjs +2 -0
  46. package/dist/index.d.mts +2 -0
  47. package/dist/index.mjs +1 -0
  48. package/package.json +8 -1
  49. package/skills/manifest.json +24 -18
  50. package/skills/stacked-prs/SKILL.md +43 -0
@@ -0,0 +1,242 @@
1
+ id: no-three-sequential-awaits
2
+ language: TypeScript
3
+ severity: error
4
+ message: 'Avoid 3 sequential await statements; abstract dependent work or parallelize independent work with Promise.all().'
5
+ files:
6
+ - '**/*.ts'
7
+ - '**/*.mts'
8
+ - '**/*.cts'
9
+ ignores:
10
+ - '**/*.test.ts'
11
+ - '**/*.test.mts'
12
+ - '**/*.test.cts'
13
+ - '**/*.spec.ts'
14
+ - '**/*.spec.mts'
15
+ - '**/*.spec.cts'
16
+ - '**/*.mock.test.ts'
17
+ - '**/*.mock.test.mts'
18
+ - '**/*.mock.test.cts'
19
+ - '**/*.stories.ts'
20
+ - '**/*.stories.mts'
21
+ - 'backend/test-helpers/**'
22
+ - 'test-helpers/**'
23
+ - 'test-infra/**'
24
+ - 'web/test-helpers/**'
25
+ - '**/test-helpers/**'
26
+ - 'playwright/**'
27
+ - 'playwright/setup/**'
28
+ - 'integration-tests/**'
29
+ - 'seed/**'
30
+ - '**/seeds/**'
31
+ - 'backend/scripts/seeds/**'
32
+ - 'backend/scripts/seed/**'
33
+ - 'backend/scripts/explain-analyze/**'
34
+ rule:
35
+ all:
36
+ - not:
37
+ follows:
38
+ stopBy: neighbor
39
+ any:
40
+ - all:
41
+ - pattern:
42
+ context: await $P
43
+ selector: expression_statement
44
+ - not:
45
+ has:
46
+ pattern: Promise.all($$)
47
+ - not:
48
+ has:
49
+ pattern: Promise.allSettled($$)
50
+ - all:
51
+ - pattern: const $W = await $P
52
+ - not:
53
+ pattern: const $W = await Promise.all($$$)
54
+ - not:
55
+ pattern: const $W = await Promise.allSettled($$$)
56
+ - all:
57
+ - pattern: let $W = await $P
58
+ - not:
59
+ pattern: let $W = await Promise.all($$$)
60
+ - not:
61
+ pattern: let $W = await Promise.allSettled($$$)
62
+ - all:
63
+ - pattern: var $W = await $P
64
+ - not:
65
+ pattern: var $W = await Promise.all($$$)
66
+ - not:
67
+ pattern: var $W = await Promise.allSettled($$$)
68
+ - all:
69
+ - pattern:
70
+ context: $W = await $P
71
+ selector: expression_statement
72
+ - not:
73
+ pattern: $W = await Promise.all($$$)
74
+ - not:
75
+ pattern: $W = await Promise.allSettled($$$)
76
+ - any:
77
+ - all:
78
+ - pattern:
79
+ context: await $A
80
+ selector: expression_statement
81
+ - not:
82
+ has:
83
+ pattern: Promise.all($$)
84
+ - not:
85
+ has:
86
+ pattern: Promise.allSettled($$)
87
+ - all:
88
+ - pattern: const $X = await $A
89
+ - not:
90
+ pattern: const $X = await Promise.all($$$)
91
+ - not:
92
+ pattern: const $X = await Promise.allSettled($$$)
93
+ - all:
94
+ - pattern: let $X = await $A
95
+ - not:
96
+ pattern: let $X = await Promise.all($$$)
97
+ - not:
98
+ pattern: let $X = await Promise.allSettled($$$)
99
+ - all:
100
+ - pattern: var $X = await $A
101
+ - not:
102
+ pattern: var $X = await Promise.all($$$)
103
+ - not:
104
+ pattern: var $X = await Promise.allSettled($$$)
105
+ - all:
106
+ - pattern:
107
+ context: $X = await $A
108
+ selector: expression_statement
109
+ - not:
110
+ pattern: $X = await Promise.all($$$)
111
+ - not:
112
+ pattern: $X = await Promise.allSettled($$$)
113
+ - precedes:
114
+ stopBy: neighbor
115
+ all:
116
+ - any:
117
+ - all:
118
+ - pattern:
119
+ context: await $B
120
+ selector: expression_statement
121
+ - not:
122
+ has:
123
+ pattern: Promise.all($$)
124
+ - not:
125
+ has:
126
+ pattern: Promise.allSettled($$)
127
+ - all:
128
+ - pattern: const $Y = await $B
129
+ - not:
130
+ pattern: const $Y = await Promise.all($$$)
131
+ - not:
132
+ pattern: const $Y = await Promise.allSettled($$$)
133
+ - all:
134
+ - pattern: let $Y = await $B
135
+ - not:
136
+ pattern: let $Y = await Promise.all($$$)
137
+ - not:
138
+ pattern: let $Y = await Promise.allSettled($$$)
139
+ - all:
140
+ - pattern: var $Y = await $B
141
+ - not:
142
+ pattern: var $Y = await Promise.all($$$)
143
+ - not:
144
+ pattern: var $Y = await Promise.allSettled($$$)
145
+ - all:
146
+ - pattern:
147
+ context: $Y = await $B
148
+ selector: expression_statement
149
+ - not:
150
+ pattern: $Y = await Promise.all($$$)
151
+ - not:
152
+ pattern: $Y = await Promise.allSettled($$$)
153
+ - precedes:
154
+ stopBy: neighbor
155
+ any:
156
+ - all:
157
+ - pattern:
158
+ context: await $C
159
+ selector: expression_statement
160
+ - not:
161
+ has:
162
+ pattern: Promise.all($$)
163
+ - not:
164
+ has:
165
+ pattern: Promise.allSettled($$)
166
+ - all:
167
+ - pattern: const $Z = await $C
168
+ - not:
169
+ pattern: const $Z = await Promise.all($$$)
170
+ - not:
171
+ pattern: const $Z = await Promise.allSettled($$$)
172
+ - all:
173
+ - pattern: let $Z = await $C
174
+ - not:
175
+ pattern: let $Z = await Promise.all($$$)
176
+ - not:
177
+ pattern: let $Z = await Promise.allSettled($$$)
178
+ - all:
179
+ - pattern: var $Z = await $C
180
+ - not:
181
+ pattern: var $Z = await Promise.all($$$)
182
+ - not:
183
+ pattern: var $Z = await Promise.allSettled($$$)
184
+ - all:
185
+ - pattern:
186
+ context: $Z = await $C
187
+ selector: expression_statement
188
+ - not:
189
+ pattern: $Z = await Promise.all($$$)
190
+ - not:
191
+ pattern: $Z = await Promise.allSettled($$$)
192
+ constraints:
193
+ A:
194
+ not:
195
+ regex: '^Promise\.all(Settled)?\b'
196
+ B:
197
+ not:
198
+ regex: '^Promise\.all(Settled)?\b'
199
+ C:
200
+ not:
201
+ regex: '^Promise\.all(Settled)?\b'
202
+ P:
203
+ not:
204
+ regex: '^Promise\.all(Settled)?\b'
205
+ examples:
206
+ - code: |
207
+ async function run() {
208
+ await one()
209
+ await two()
210
+ await three()
211
+ }
212
+ isValid: false
213
+ file: 'backend/services/example.mts'
214
+ - code: |
215
+ async function run() {
216
+ const one = await loadOne()
217
+ const two = await loadTwo()
218
+ target.value = await loadThree()
219
+ }
220
+ isValid: false
221
+ file: 'backend/services/example.mts'
222
+ - code: |
223
+ async function run() {
224
+ await one()
225
+ await two()
226
+ }
227
+ isValid: true
228
+ file: 'backend/services/example.mts'
229
+ - code: |
230
+ async function run() {
231
+ await one()
232
+ if (condition) await two()
233
+ await three()
234
+ }
235
+ isValid: true
236
+ file: 'backend/services/example.mts'
237
+ - code: |
238
+ async function run() {
239
+ await Promise.all([one(), two(), three()])
240
+ }
241
+ isValid: true
242
+ file: 'backend/services/example.mts'
@@ -0,0 +1,429 @@
1
+ id: nullable-options
2
+ language: TypeScript
3
+ severity: error
4
+ message: >-
5
+ This helper option member is declared optional-nullable (`T | null` with `?`), so a bare `??`/`||`
6
+ default silently erases an explicit `null` the same way it erases `undefined`. Use an explicit
7
+ `!== undefined` check so `null` is preserved, or add a `null-default-ok` (or `null intentionally
8
+ defaults`) allowlist comment on the line above (or the same line) if erasing `null` is intentional.
9
+ files:
10
+ - 'backend/test-helpers/**/*.mts'
11
+ - 'backend/test-helpers/**/*.ts'
12
+ - 'test-helpers/**/*.mts'
13
+ - 'test-helpers/**/*.ts'
14
+ ignores:
15
+ - '**/*.test.mts'
16
+ - '**/*.test.ts'
17
+ - '**/*.spec.mts'
18
+ - '**/*.spec.ts'
19
+ - '**/*.mock.test.mts'
20
+ - '**/*.mock.test.ts'
21
+ utils:
22
+ # A direct null-ish type member: either the bare `null` keyword type, or a `literal_type` node
23
+ # wrapping it (tree-sitter wraps `null` in `literal_type` when it appears as a union member).
24
+ null-member:
25
+ any:
26
+ - kind: 'null'
27
+ - all:
28
+ - kind: literal_type
29
+ - has:
30
+ stopBy: neighbor
31
+ kind: 'null'
32
+ # Matches a `type_annotation` node only when its DIRECT type value -- or, if that direct value is
33
+ # a union type, one of its direct members -- is null-ish. Deliberately does NOT descend into
34
+ # unrelated nested generic/object types (e.g. `Array<string | null>` must NOT match: that `null`
35
+ # is nested inside the generic's type argument, not the property's own top-level union), mirroring
36
+ # the original `directTypeContainsNull` walker's union-member-only recursion.
37
+ nullable-property-type:
38
+ has:
39
+ stopBy: neighbor
40
+ any:
41
+ - matches: null-member
42
+ - all:
43
+ - kind: union_type
44
+ - has:
45
+ stopBy: neighbor
46
+ matches: null-member
47
+ # A `property_signature` declaring $PROP as optional (`?:`) with a nullable type. Reused for
48
+ # inline object-type literals and for same-file interface/type-alias bodies, so the
49
+ # nullable-member detection logic lives in exactly one place instead of being duplicated per call
50
+ # site.
51
+ declares-nullable-property:
52
+ all:
53
+ - kind: property_signature
54
+ - regex: '\?\s*:'
55
+ - has:
56
+ field: name
57
+ kind: property_identifier
58
+ pattern: $PROP
59
+ - has:
60
+ field: type
61
+ matches: nullable-property-type
62
+ # `$OBJ.$PROP` (dot notation) or `$OBJ['$PROP']` / `$OBJ["$PROP"]` (bracket notation with a
63
+ # string-literal key), mirroring the original `memberLabel` helper which resolved both forms via
64
+ # `propertyName` (Identifier or string Literal).
65
+ nullable-member-access:
66
+ any:
67
+ - all:
68
+ - kind: member_expression
69
+ - has:
70
+ field: object
71
+ kind: identifier
72
+ pattern: $OBJ
73
+ - has:
74
+ field: property
75
+ kind: property_identifier
76
+ pattern: $PROP
77
+ - all:
78
+ - kind: subscript_expression
79
+ - has:
80
+ field: object
81
+ kind: identifier
82
+ pattern: $OBJ
83
+ - has:
84
+ field: index
85
+ kind: string
86
+ has:
87
+ kind: string_fragment
88
+ pattern: $PROP
89
+ # A `type_identifier` naming a type directly, or the `name` of a `generic_type` (e.g. the
90
+ # `Options` in `Options<Date>`) -- tree-sitter wraps a referenced type's own name one level deeper
91
+ # when the reference carries type arguments, unlike an ESTree `TSTypeReference` whose `typeName`
92
+ # is unaffected by generics.
93
+ type-reference-name:
94
+ any:
95
+ - kind: type_identifier
96
+ pattern: $TYPENAME
97
+ - all:
98
+ - kind: generic_type
99
+ - has:
100
+ field: name
101
+ kind: type_identifier
102
+ pattern: $TYPENAME
103
+ # A same-file interface or type-alias declaration named $TYPENAME whose body/value declares $PROP
104
+ # as an optional-nullable member.
105
+ declares-nullable-property-via-reference:
106
+ inside:
107
+ stopBy: end
108
+ kind: program
109
+ has:
110
+ stopBy: end
111
+ any:
112
+ - all:
113
+ - kind: interface_declaration
114
+ - has:
115
+ field: name
116
+ pattern: $TYPENAME
117
+ - has:
118
+ field: body
119
+ stopBy: end
120
+ has:
121
+ stopBy: end
122
+ matches: declares-nullable-property
123
+ - all:
124
+ - kind: type_alias_declaration
125
+ - has:
126
+ field: name
127
+ pattern: $TYPENAME
128
+ - has:
129
+ field: value
130
+ stopBy: end
131
+ has:
132
+ stopBy: end
133
+ matches: declares-nullable-property
134
+ # Case A only: the matched member/subscript access must be inside a function whose OWN
135
+ # required_parameter binds both $OBJ (the parameter name) and the nullable-$PROP type (inline
136
+ # literal, or a same-file interface/type-alias reference) on the SAME parameter -- so two
137
+ # differently-typed parameters that happen to share a property name can't cross-associate.
138
+ declared-nullable-type-for-obj:
139
+ inside:
140
+ stopBy: end
141
+ all:
142
+ - any:
143
+ - kind: function_declaration
144
+ - kind: arrow_function
145
+ - kind: function_expression
146
+ - kind: method_definition
147
+ - has:
148
+ stopBy: end
149
+ kind: required_parameter
150
+ all:
151
+ - has:
152
+ field: pattern
153
+ kind: identifier
154
+ pattern: $OBJ
155
+ - any:
156
+ - has:
157
+ field: type
158
+ has:
159
+ stopBy: end
160
+ matches: declares-nullable-property
161
+ - all:
162
+ - has:
163
+ field: type
164
+ has:
165
+ matches: type-reference-name
166
+ - matches: declares-nullable-property-via-reference
167
+ # An allowlist comment (`null-default-ok` / `null intentionally defaults`) adjacent to the
168
+ # enclosing statement: either on the line above (a leading comment attaches as the PRECEDING
169
+ # sibling of the statement within its parent's children), or trailing on the same line (a
170
+ # same-line trailing comment attaches as the LAST CHILD of the statement itself, not as a
171
+ # following sibling -- confirmed via `ast-grep --debug-query=cst`).
172
+ null-default-ok-adjacent:
173
+ any:
174
+ - follows:
175
+ stopBy: neighbor
176
+ kind: comment
177
+ regex: '(?i)null-default-ok|null intentionally defaults'
178
+ - has:
179
+ stopBy: neighbor
180
+ kind: comment
181
+ regex: '(?i)null-default-ok|null intentionally defaults'
182
+ rule:
183
+ any:
184
+ # Case A: `$OBJ.$PROP` / `$OBJ['$PROP']` default (member/subscript expression, including the
185
+ # `??=`/`||=` compound-assignment forms) where $OBJ is a known helper option-object parameter
186
+ # whose annotated type (inline literal or same-file interface/type-alias reference) declares
187
+ # $PROP as optional-nullable.
188
+ - all:
189
+ - any:
190
+ - all:
191
+ - kind: binary_expression
192
+ - has:
193
+ field: operator
194
+ regex: '^(?:\?\?|\|\|)$'
195
+ - has:
196
+ field: left
197
+ matches: nullable-member-access
198
+ - has:
199
+ field: right
200
+ not:
201
+ kind: 'null'
202
+ - all:
203
+ - kind: augmented_assignment_expression
204
+ - has:
205
+ field: operator
206
+ regex: '^(?:\?\?=|\|\|=)$'
207
+ - has:
208
+ field: left
209
+ matches: nullable-member-access
210
+ - has:
211
+ field: right
212
+ not:
213
+ kind: 'null'
214
+ - matches: declared-nullable-type-for-obj
215
+ - not:
216
+ inside:
217
+ stopBy: end
218
+ all:
219
+ - any:
220
+ - kind: lexical_declaration
221
+ - kind: variable_declaration
222
+ - kind: expression_statement
223
+ - kind: return_statement
224
+ - kind: function_declaration
225
+ - kind: arrow_function
226
+ - kind: function_expression
227
+ - kind: method_definition
228
+ - kind: export_statement
229
+ - matches: null-default-ok-adjacent
230
+ # Case B: destructured object-pattern default (`{ $PROP = $DEFAULT }` or the renamed/aliased
231
+ # form `{ $PROP: $ALIAS = $DEFAULT }`) inside a function parameter, where the parameter's own
232
+ # type annotation (inline literal or same-file interface/type-alias reference) declares $PROP as
233
+ # optional-nullable. Unlike Case A, the destructured object itself has no name to constrain
234
+ # against the helper-option-object allowlist: this matches the original
235
+ # `checkObjectPatternDefaults`, which applies to every function parameter regardless of the
236
+ # destructured variable's own name. Because the matched node is always nested directly inside
237
+ # its own `required_parameter` (via `object_pattern`), an ancestor search for that
238
+ # `required_parameter` is inherently scoped to the correct parameter -- no separate $OBJ/$PROP
239
+ # binding is needed the way Case A needs one.
240
+ - all:
241
+ - any:
242
+ - all:
243
+ - kind: object_assignment_pattern
244
+ - has:
245
+ field: left
246
+ kind: shorthand_property_identifier_pattern
247
+ pattern: $PROP
248
+ - has:
249
+ field: right
250
+ not:
251
+ kind: 'null'
252
+ - all:
253
+ - kind: pair_pattern
254
+ - has:
255
+ field: key
256
+ kind: property_identifier
257
+ pattern: $PROP
258
+ - has:
259
+ field: value
260
+ kind: assignment_pattern
261
+ has:
262
+ field: right
263
+ not:
264
+ kind: 'null'
265
+ - inside:
266
+ stopBy: end
267
+ kind: required_parameter
268
+ any:
269
+ - has:
270
+ field: type
271
+ has:
272
+ stopBy: end
273
+ matches: declares-nullable-property
274
+ - all:
275
+ - has:
276
+ field: type
277
+ has:
278
+ matches: type-reference-name
279
+ - matches: declares-nullable-property-via-reference
280
+ - not:
281
+ inside:
282
+ stopBy: end
283
+ all:
284
+ - any:
285
+ - kind: lexical_declaration
286
+ - kind: variable_declaration
287
+ - kind: expression_statement
288
+ - kind: return_statement
289
+ - kind: function_declaration
290
+ - kind: arrow_function
291
+ - kind: function_expression
292
+ - kind: method_definition
293
+ - kind: export_statement
294
+ - matches: null-default-ok-adjacent
295
+ constraints:
296
+ OBJ:
297
+ regex: '^(?:data|input|options|params)$'
298
+ examples:
299
+ - code: |
300
+ export function makeRow(data: { completedAt?: Date | null }) {
301
+ return { completed_at: data.completedAt ?? new Date() }
302
+ }
303
+ isValid: false
304
+ file: 'backend/test-helpers/entities/nullable-options-inline.mts'
305
+ - code: |
306
+ interface InterfaceOptions {
307
+ completedAt?: Date | null
308
+ }
309
+
310
+ export function makeInterfaceRow(options: InterfaceOptions) {
311
+ return { completed_at: options.completedAt || new Date() }
312
+ }
313
+ isValid: false
314
+ file: 'backend/test-helpers/entities/nullable-options-interface.mts'
315
+ - code: |
316
+ type TypeOptions = {
317
+ closedAt?: Date | null
318
+ }
319
+
320
+ export function makeTypeRow(params: TypeOptions) {
321
+ params.closedAt ??= new Date()
322
+ return params
323
+ }
324
+ isValid: false
325
+ file: 'backend/test-helpers/entities/nullable-options-type-alias.mts'
326
+ - code: |
327
+ type MakeRowOptions = {
328
+ completedAt?: Date | null
329
+ }
330
+
331
+ export function makeRow({ completedAt = new Date() }: MakeRowOptions) {
332
+ return { completed_at: completedAt }
333
+ }
334
+ isValid: false
335
+ file: 'backend/test-helpers/entities/nullable-options-destructured.mts'
336
+ - code: |
337
+ type MakeRowOptions = {
338
+ completedAt?: Date | null
339
+ }
340
+
341
+ export function makeRow({ completedAt: closedAt = new Date() }: MakeRowOptions) {
342
+ return { completed_at: closedAt }
343
+ }
344
+ isValid: false
345
+ file: 'backend/test-helpers/entities/nullable-options-destructured-aliased.mts'
346
+ - code: |
347
+ export function makeRow(data: { completedAt?: Date | null }) {
348
+ return { completed_at: data['completedAt'] ?? new Date() }
349
+ }
350
+ isValid: false
351
+ file: 'backend/test-helpers/entities/nullable-options-bracket-notation.mts'
352
+ - code: |
353
+ type MakeRowOptions<T> = {
354
+ completedAt?: T | null
355
+ }
356
+
357
+ export function makeRow(options: MakeRowOptions<Date>) {
358
+ return { completed_at: options.completedAt ?? new Date() }
359
+ }
360
+ isValid: false
361
+ file: 'backend/test-helpers/entities/nullable-options-generic-reference.mts'
362
+ - code: |
363
+ export function makeRow(data: { completedAt?: Date | null; closedAt?: Date | null }) {
364
+ const completedAt = data.completedAt !== undefined ? data.completedAt : new Date()
365
+ const userErrorText = data.completedAt ?? null
366
+ return { completedAt, userErrorText }
367
+ }
368
+ isValid: true
369
+ file: 'backend/test-helpers/entities/nullable-options-explicit-check.mts'
370
+ - code: |
371
+ export function makeRow(data: { closedAt?: Date | null }) {
372
+ // null-default-ok: this fixture intentionally treats null like missing input.
373
+ const closedAt = data.closedAt ?? new Date()
374
+ return { closedAt }
375
+ }
376
+ isValid: true
377
+ file: 'backend/test-helpers/entities/nullable-options-allowlisted.mts'
378
+ - code: |
379
+ export function makeRow(data: { closedAt?: Date | null }) {
380
+ const closedAt = data.closedAt ?? new Date() // null-default-ok: same-line trailing form.
381
+ return { closedAt }
382
+ }
383
+ isValid: true
384
+ file: 'backend/test-helpers/entities/nullable-options-allowlisted-trailing.mts'
385
+ - code: |
386
+ import type { ExternalOptions } from './types.mts'
387
+
388
+ export function makeImportedRow(options: ExternalOptions) {
389
+ return { completed_at: options.completedAt ?? new Date() }
390
+ }
391
+ isValid: true
392
+ file: 'backend/test-helpers/entities/nullable-options-imported-type.mts'
393
+ - code: |
394
+ type LocalOptions = {
395
+ completedAt?: Date | null
396
+ }
397
+
398
+ export function makeUnionRow(options: LocalOptions | undefined) {
399
+ return { completed_at: options?.completedAt ?? new Date() }
400
+ }
401
+ isValid: true
402
+ file: 'backend/test-helpers/entities/nullable-options-union-param.mts'
403
+ - code: |
404
+ export function makeRow(data: { values?: Array<string | null> }) {
405
+ return { values: data.values ?? [] }
406
+ }
407
+ isValid: true
408
+ file: 'backend/test-helpers/entities/nullable-options-nested-union-not-flagged.mts'
409
+ - code: |
410
+ type FooType = { completedAt?: Date | null }
411
+ type BarType = { completedAt: Date }
412
+
413
+ export function makeRow(data: BarType, unrelated: FooType) {
414
+ return { completed_at: data.completedAt ?? new Date() }
415
+ }
416
+ isValid: true
417
+ file: 'backend/test-helpers/entities/nullable-options-cross-param-not-flagged.mts'
418
+ - code: |
419
+ export function makeRow(data: { completedAt?: Date | null }) {
420
+ return { completed_at: data.completedAt ?? new Date() }
421
+ }
422
+ isValid: true
423
+ file: 'backend/services/nullable-options-out-of-scope.mts'
424
+ - code: |
425
+ export function makeRow(data: { completedAt?: Date | null }) {
426
+ return { completed_at: data.completedAt ?? new Date() }
427
+ }
428
+ isValid: true
429
+ file: 'backend/test-helpers/entities/nullable-options.test.mts'