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.
- package/README.md +4 -0
- package/ast-grep/rules/backend-no-test-file-imports.yml +44 -0
- package/ast-grep/rules/force-dynamic-layout.yml +57 -0
- package/ast-grep/rules/force-dynamic-pages.yml +38 -0
- package/ast-grep/rules/no-dynamic-import-in-vitest-config.yml +30 -0
- package/ast-grep/rules/no-dynamic-server-components-tsx.yml +71 -0
- package/ast-grep/rules/no-dynamic-server-components.yml +71 -0
- package/ast-grep/rules/no-enable-indexscan-off-tsx.yml +33 -0
- package/ast-grep/rules/no-enable-indexscan-off.yml +41 -0
- package/ast-grep/rules/no-exports-in-tests-folder-tsx.yml +42 -0
- package/ast-grep/rules/no-exports-in-tests-folder.yml +43 -0
- package/ast-grep/rules/no-object-assign-new-error.yml +31 -0
- package/ast-grep/rules/no-runner-temp-nullish.yml +33 -0
- package/ast-grep/rules/no-self-comparison-assertion-tsx.yml +43 -0
- package/ast-grep/rules/no-self-comparison-assertion.yml +53 -0
- package/ast-grep/rules/no-sql-offset.yml +65 -0
- package/ast-grep/rules/no-sync-throw-assert-on-async-enqueue.yml +42 -0
- package/ast-grep/rules/no-tautological-literal-assertion-tsx.yml +31 -0
- package/ast-grep/rules/no-tautological-literal-assertion.yml +38 -0
- package/ast-grep/rules/no-test-max-lines-disable-tsx.yml +41 -0
- package/ast-grep/rules/no-test-max-lines-disable.yml +53 -0
- package/ast-grep/rules/no-three-sequential-awaits-tsx.yml +288 -0
- package/ast-grep/rules/no-three-sequential-awaits.yml +242 -0
- package/ast-grep/rules/nullable-options.yml +429 -0
- package/ast-grep/rules/pagination-no-exact-limit-has-next.yml +111 -0
- package/ast-grep/rules/playwright-no-bare-meta-press.yml +29 -0
- package/ast-grep/rules/storybook-no-webpack-final.yml +31 -0
- package/ast-grep/rules/web-class-component-needs-use-client.yml +175 -0
- package/ast-grep/rules/web-clickable-needs-pointer.yml +74 -0
- package/ast-grep/rules/web-dialog-content-description.yml +56 -0
- package/ast-grep/rules/web-icon-button-tooltip.yml +82 -0
- package/ast-grep/rules/web-no-inline-scrollbar-hide.yml +41 -0
- package/ast-grep/rules/web-no-window-open-navigation.yml +39 -0
- package/ast-grep/sgconfig.yml +9 -0
- package/dist/ast-grep-pack/index.d.mts +6 -0
- package/dist/ast-grep-pack/index.mjs +11 -0
- package/dist/cli/commands/ast-grep-pack.d.mts +2 -0
- package/dist/cli/commands/ast-grep-pack.mjs +11 -0
- package/dist/cli/index.mjs +3 -0
- package/dist/cli/parse-options.d.mts +1 -0
- package/dist/cli/parse-options.mjs +6 -0
- package/dist/cli/parse.d.mts +2 -0
- package/dist/cli/parse.mjs +3 -1
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +2 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +1 -0
- package/package.json +8 -1
- package/skills/manifest.json +24 -18
- 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'
|