vouchington-tooling 0.14.0 → 0.15.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 (27) hide show
  1. package/README.md +3 -0
  2. package/ast-grep/rules/backend-no-test-file-imports.yml +1 -1
  3. package/ast-grep/rules/no-dynamic-import-in-vitest-config.yml +1 -1
  4. package/ast-grep/rules/no-dynamic-server-components.yml +41 -5
  5. package/ast-grep/rules/no-enable-indexscan-off.yml +17 -4
  6. package/ast-grep/rules/no-exports-in-tests-folder.yml +27 -4
  7. package/ast-grep/rules/no-object-assign-new-error.yml +1 -1
  8. package/ast-grep/rules/no-runner-temp-nullish.yml +1 -1
  9. package/ast-grep/rules/no-self-comparison-assertion.yml +21 -1
  10. package/ast-grep/rules/no-sql-offset.yml +1 -1
  11. package/ast-grep/rules/no-sync-throw-assert-on-async-enqueue.yml +1 -1
  12. package/ast-grep/rules/no-tautological-literal-assertion.yml +15 -1
  13. package/ast-grep/rules/no-test-max-lines-disable.yml +26 -4
  14. package/ast-grep/rules/no-three-sequential-awaits.yml +220 -12
  15. package/ast-grep/rules/nullable-options.yml +1 -1
  16. package/ast-grep/rules/pagination-no-exact-limit-has-next.yml +1 -1
  17. package/ast-grep/rules/playwright-no-bare-meta-press.yml +1 -1
  18. package/ast-grep/rules/storybook-no-webpack-final.yml +1 -1
  19. package/ast-grep/sgconfig.yml +1 -2
  20. package/package.json +1 -1
  21. package/ast-grep/rules/no-dynamic-server-components-tsx.yml +0 -71
  22. package/ast-grep/rules/no-enable-indexscan-off-tsx.yml +0 -33
  23. package/ast-grep/rules/no-exports-in-tests-folder-tsx.yml +0 -42
  24. package/ast-grep/rules/no-self-comparison-assertion-tsx.yml +0 -43
  25. package/ast-grep/rules/no-tautological-literal-assertion-tsx.yml +0 -31
  26. package/ast-grep/rules/no-test-max-lines-disable-tsx.yml +0 -41
  27. package/ast-grep/rules/no-three-sequential-awaits-tsx.yml +0 -466
@@ -1,11 +1,12 @@
1
1
  id: no-three-sequential-awaits
2
- language: TypeScript
2
+ language: Tsx
3
3
  severity: error
4
4
  message: 'Avoid 3 sequential await statements; abstract dependent work or parallelize independent work with Promise.all().'
5
5
  files:
6
6
  - '**/*.ts'
7
7
  - '**/*.mts'
8
8
  - '**/*.cts'
9
+ - '**/*.tsx'
9
10
  ignores:
10
11
  - '**/*.test.ts'
11
12
  - '**/*.test.mts'
@@ -18,19 +19,23 @@ ignores:
18
19
  - '**/*.mock.test.cts'
19
20
  - '**/*.stories.ts'
20
21
  - '**/*.stories.mts'
21
- - 'backend/test-helpers/**'
22
- - 'test-helpers/**'
23
- - 'test-infra/**'
24
- - 'web/test-helpers/**'
22
+ - backend/test-helpers/**
23
+ - test-helpers/**
24
+ - test-infra/**
25
+ - web/test-helpers/**
25
26
  - '**/test-helpers/**'
26
- - 'playwright/**'
27
- - 'playwright/setup/**'
28
- - 'integration-tests/**'
29
- - 'seed/**'
27
+ - playwright/**
28
+ - playwright/setup/**
29
+ - integration-tests/**
30
+ - seed/**
30
31
  - '**/seeds/**'
31
- - 'backend/scripts/seeds/**'
32
- - 'backend/scripts/seed/**'
33
- - 'backend/scripts/explain-analyze/**'
32
+ - backend/scripts/seeds/**
33
+ - backend/scripts/seed/**
34
+ - backend/scripts/explain-analyze/**
35
+ - '**/*.test.tsx'
36
+ - '**/*.spec.tsx'
37
+ - '**/*.mock.test.tsx'
38
+ - '**/*.stories.tsx'
34
39
  rule:
35
40
  all:
36
41
  - not:
@@ -45,6 +50,10 @@ rule:
45
50
  - pattern:
46
51
  context: await $P
47
52
  selector: expression_statement
53
+ - not:
54
+ pattern: await params
55
+ - not:
56
+ pattern: await searchParams
48
57
  - not:
49
58
  has:
50
59
  pattern: Promise.all($$)
@@ -53,18 +62,30 @@ rule:
53
62
  pattern: Promise.allSettled($$)
54
63
  - all:
55
64
  - pattern: const $W = await $P
65
+ - not:
66
+ pattern: const $W = await params
67
+ - not:
68
+ pattern: const $W = await searchParams
56
69
  - not:
57
70
  pattern: const $W = await Promise.all($$$)
58
71
  - not:
59
72
  pattern: const $W = await Promise.allSettled($$$)
60
73
  - all:
61
74
  - pattern: let $W = await $P
75
+ - not:
76
+ pattern: let $W = await params
77
+ - not:
78
+ pattern: let $W = await searchParams
62
79
  - not:
63
80
  pattern: let $W = await Promise.all($$$)
64
81
  - not:
65
82
  pattern: let $W = await Promise.allSettled($$$)
66
83
  - all:
67
84
  - pattern: var $W = await $P
85
+ - not:
86
+ pattern: var $W = await params
87
+ - not:
88
+ pattern: var $W = await searchParams
68
89
  - not:
69
90
  pattern: var $W = await Promise.all($$$)
70
91
  - not:
@@ -73,6 +94,10 @@ rule:
73
94
  - pattern:
74
95
  context: $W = await $P
75
96
  selector: expression_statement
97
+ - not:
98
+ pattern: $W = await params
99
+ - not:
100
+ pattern: $W = await searchParams
76
101
  - not:
77
102
  pattern: $W = await Promise.all($$$)
78
103
  - not:
@@ -86,6 +111,10 @@ rule:
86
111
  - pattern:
87
112
  context: await $A
88
113
  selector: expression_statement
114
+ - not:
115
+ pattern: await params
116
+ - not:
117
+ pattern: await searchParams
89
118
  - not:
90
119
  has:
91
120
  pattern: Promise.all($$)
@@ -94,18 +123,30 @@ rule:
94
123
  pattern: Promise.allSettled($$)
95
124
  - all:
96
125
  - pattern: const $X = await $A
126
+ - not:
127
+ pattern: const $X = await params
128
+ - not:
129
+ pattern: const $X = await searchParams
97
130
  - not:
98
131
  pattern: const $X = await Promise.all($$$)
99
132
  - not:
100
133
  pattern: const $X = await Promise.allSettled($$$)
101
134
  - all:
102
135
  - pattern: let $X = await $A
136
+ - not:
137
+ pattern: let $X = await params
138
+ - not:
139
+ pattern: let $X = await searchParams
103
140
  - not:
104
141
  pattern: let $X = await Promise.all($$$)
105
142
  - not:
106
143
  pattern: let $X = await Promise.allSettled($$$)
107
144
  - all:
108
145
  - pattern: var $X = await $A
146
+ - not:
147
+ pattern: var $X = await params
148
+ - not:
149
+ pattern: var $X = await searchParams
109
150
  - not:
110
151
  pattern: var $X = await Promise.all($$$)
111
152
  - not:
@@ -114,6 +155,10 @@ rule:
114
155
  - pattern:
115
156
  context: $X = await $A
116
157
  selector: expression_statement
158
+ - not:
159
+ pattern: $X = await params
160
+ - not:
161
+ pattern: $X = await searchParams
117
162
  - not:
118
163
  pattern: $X = await Promise.all($$$)
119
164
  - not:
@@ -126,6 +171,10 @@ rule:
126
171
  - pattern:
127
172
  context: await $B
128
173
  selector: expression_statement
174
+ - not:
175
+ pattern: await params
176
+ - not:
177
+ pattern: await searchParams
129
178
  - not:
130
179
  has:
131
180
  pattern: Promise.all($$)
@@ -134,18 +183,30 @@ rule:
134
183
  pattern: Promise.allSettled($$)
135
184
  - all:
136
185
  - pattern: const $Y = await $B
186
+ - not:
187
+ pattern: const $Y = await params
188
+ - not:
189
+ pattern: const $Y = await searchParams
137
190
  - not:
138
191
  pattern: const $Y = await Promise.all($$$)
139
192
  - not:
140
193
  pattern: const $Y = await Promise.allSettled($$$)
141
194
  - all:
142
195
  - pattern: let $Y = await $B
196
+ - not:
197
+ pattern: let $Y = await params
198
+ - not:
199
+ pattern: let $Y = await searchParams
143
200
  - not:
144
201
  pattern: let $Y = await Promise.all($$$)
145
202
  - not:
146
203
  pattern: let $Y = await Promise.allSettled($$$)
147
204
  - all:
148
205
  - pattern: var $Y = await $B
206
+ - not:
207
+ pattern: var $Y = await params
208
+ - not:
209
+ pattern: var $Y = await searchParams
149
210
  - not:
150
211
  pattern: var $Y = await Promise.all($$$)
151
212
  - not:
@@ -154,6 +215,10 @@ rule:
154
215
  - pattern:
155
216
  context: $Y = await $B
156
217
  selector: expression_statement
218
+ - not:
219
+ pattern: $Y = await params
220
+ - not:
221
+ pattern: $Y = await searchParams
157
222
  - not:
158
223
  pattern: $Y = await Promise.all($$$)
159
224
  - not:
@@ -173,6 +238,10 @@ rule:
173
238
  - pattern:
174
239
  context: await $C
175
240
  selector: expression_statement
241
+ - not:
242
+ pattern: await params
243
+ - not:
244
+ pattern: await searchParams
176
245
  - not:
177
246
  has:
178
247
  pattern: Promise.all($$)
@@ -181,18 +250,30 @@ rule:
181
250
  pattern: Promise.allSettled($$)
182
251
  - all:
183
252
  - pattern: const $Z = await $C
253
+ - not:
254
+ pattern: const $Z = await params
255
+ - not:
256
+ pattern: const $Z = await searchParams
184
257
  - not:
185
258
  pattern: const $Z = await Promise.all($$$)
186
259
  - not:
187
260
  pattern: const $Z = await Promise.allSettled($$$)
188
261
  - all:
189
262
  - pattern: let $Z = await $C
263
+ - not:
264
+ pattern: let $Z = await params
265
+ - not:
266
+ pattern: let $Z = await searchParams
190
267
  - not:
191
268
  pattern: let $Z = await Promise.all($$$)
192
269
  - not:
193
270
  pattern: let $Z = await Promise.allSettled($$$)
194
271
  - all:
195
272
  - pattern: var $Z = await $C
273
+ - not:
274
+ pattern: var $Z = await params
275
+ - not:
276
+ pattern: var $Z = await searchParams
196
277
  - not:
197
278
  pattern: var $Z = await Promise.all($$$)
198
279
  - not:
@@ -201,6 +282,10 @@ rule:
201
282
  - pattern:
202
283
  context: $Z = await $C
203
284
  selector: expression_statement
285
+ - not:
286
+ pattern: $Z = await params
287
+ - not:
288
+ pattern: $Z = await searchParams
204
289
  - not:
205
290
  pattern: $Z = await Promise.all($$$)
206
291
  - not:
@@ -409,3 +494,126 @@ examples:
409
494
  }
410
495
  isValid: false
411
496
  file: 'backend/services/example.mts'
497
+ - code: |
498
+ async function run() {
499
+ await one()
500
+ await two()
501
+ await three()
502
+ }
503
+ isValid: false
504
+ file: web/app/example.tsx
505
+ - code: |
506
+ async function run() {
507
+ const { id } = await params
508
+ const topic = await getTopic(id)
509
+ const aliases = await getAliases(id)
510
+ return <div>{topic.name}{aliases.length}</div>
511
+ }
512
+ isValid: true
513
+ file: web/app/example.tsx
514
+ - code: |
515
+ async function run() {
516
+ await Promise.all([one(), two(), three()])
517
+ return <div />
518
+ }
519
+ isValid: true
520
+ file: web/app/example.tsx
521
+ - code: |
522
+ async function run() {
523
+ await using transaction = await beginTransaction()
524
+ await applyChange(transaction)
525
+ await transaction.commit()
526
+ return <div />
527
+ }
528
+ isValid: true
529
+ file: web/app/example.tsx
530
+ - code: |
531
+ async function run() {
532
+ await using transaction = await beginBoundedTransaction({ timeoutMs: 1000 })
533
+ await applyFirstChange(transaction)
534
+ await applySecondChange(transaction)
535
+ await transaction.commit()
536
+ return <div />
537
+ }
538
+ isValid: true
539
+ file: web/app/example.tsx
540
+ - code: |
541
+ async function run() {
542
+ await using transaction = await beginTransaction()
543
+ await applyFirstChange(transaction)
544
+ await applySecondChange(transaction)
545
+ await applyThirdChange(transaction)
546
+ await transaction.commit()
547
+ return <div />
548
+ }
549
+ isValid: false
550
+ file: web/app/example.tsx
551
+ - code: |
552
+ async function run() {
553
+ await unrelatedFirst()
554
+ await unrelatedSecond()
555
+ await unrelatedThird()
556
+ await using transaction = await beginTransaction()
557
+ await applyChange(transaction)
558
+ await transaction.commit()
559
+ return <div />
560
+ }
561
+ isValid: false
562
+ file: web/app/example.tsx
563
+ - code: |
564
+ async function run() {
565
+ await using transaction = await beginTransaction()
566
+ await applyFirstChange(transaction)
567
+ await applySecondChange(transaction)
568
+ await other.commit()
569
+ return <div />
570
+ }
571
+ isValid: false
572
+ file: web/app/example.tsx
573
+ - code: |
574
+ async function run() {
575
+ await using transaction = await beginTransaction()
576
+ await applyChange(transaction)
577
+ await transaction.commit()
578
+ await afterFirst()
579
+ await afterSecond()
580
+ await afterThird()
581
+ return <div />
582
+ }
583
+ isValid: false
584
+ file: web/app/example.tsx
585
+ - code: |
586
+ async function run() {
587
+ await using outer = await acquire()
588
+ {
589
+ await using inner = await acquire()
590
+ await innerFirst(inner)
591
+ await innerSecond(inner)
592
+ await innerThird(inner)
593
+ await inner.commit()
594
+ }
595
+ await outer.commit()
596
+ return <div />
597
+ }
598
+ isValid: false
599
+ file: web/app/example.tsx
600
+ - code: |
601
+ async function run() {
602
+ await using transaction = await beginTransaction()
603
+ await applyFirstChange(transaction)
604
+ await applySecondChange(transaction)
605
+ await transaction.rollback()
606
+ return <div />
607
+ }
608
+ isValid: false
609
+ file: web/app/example.tsx
610
+ - code: |
611
+ async function run() {
612
+ await using resource = await acquire()
613
+ await useFirst(resource)
614
+ await useSecond(resource)
615
+ await resource.dispose()
616
+ return <div />
617
+ }
618
+ isValid: false
619
+ file: web/app/example.tsx
@@ -1,5 +1,5 @@
1
1
  id: nullable-options
2
- language: TypeScript
2
+ language: Tsx
3
3
  severity: error
4
4
  message: >-
5
5
  This helper option member is declared optional-nullable (`T | null` with `?`), so a bare `??`/`||`
@@ -1,5 +1,5 @@
1
1
  id: pagination-no-exact-limit-has-next
2
- language: TypeScript
2
+ language: Tsx
3
3
  severity: error
4
4
  message: 'Do not infer another page from an exact page-size boundary; fetch limit + 1 and test rows.length > limit.'
5
5
  files:
@@ -1,5 +1,5 @@
1
1
  id: playwright-no-bare-meta-press
2
- language: TypeScript
2
+ language: Tsx
3
3
  severity: error
4
4
  message: "use 'ControlOrMeta+' instead of 'Meta+' in Playwright .press() calls; bare Meta+ fails on Linux CI"
5
5
  files:
@@ -1,5 +1,5 @@
1
1
  id: storybook-no-webpack-final
2
- language: TypeScript
2
+ language: Tsx
3
3
  severity: error
4
4
  message: 'Storybook must use viteFinal, not webpackFinal; this project uses @storybook/nextjs-vite'
5
5
  files:
@@ -1,9 +1,8 @@
1
1
  ruleDirs:
2
2
  - rules
3
3
  languageGlobs:
4
- TypeScript:
4
+ Tsx:
5
5
  - '**/*.ts'
6
6
  - '**/*.mts'
7
7
  - '**/*.cts'
8
- Tsx:
9
8
  - '**/*.tsx'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Vouchington CLI and extractable tooling libraries.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
6
6
  "bugs": {
@@ -1,71 +0,0 @@
1
- # NOTE: This rule flags calls to `dynamic` or `nextDynamic` (next/dynamic) that use a dynamic import.
2
- # It cannot verify the import source — it matches by function name.
3
- # If you have other functions named `dynamic` or `nextDynamic`, add a suppression comment.
4
- id: no-dynamic-server-components-tsx
5
- language: Tsx
6
- severity: error
7
- message: "dynamic()/nextDynamic() may wrap a server component — ensure the target has 'use client'"
8
- # NOTE: ast-grep maps `.tsx` to language `Tsx`, not `TypeScript`. This rule
9
- # only scans `.tsx` files; see no-dynamic-server-components.yml for the
10
- # companion rule that covers `.ts` files.
11
- files:
12
- - 'web/**/*.tsx'
13
- ignores:
14
- - '**/*.test.tsx'
15
- - '**/*.spec.tsx'
16
- rule:
17
- any:
18
- - pattern: '$FN(() => import($PATH))'
19
- - pattern: '$FN(() => { return import($PATH); })'
20
- - pattern: '$FN(() => import($PATH).then($$$))'
21
- - pattern: '$FN(() => import($PATH), $OPTS)'
22
- - pattern: '$FN(() => { return import($PATH); }, $OPTS)'
23
- - pattern: '$FN(() => import($PATH).then($$$), $OPTS)'
24
- constraints:
25
- FN:
26
- regex: '^(dynamic|nextDynamic)$'
27
- PATH:
28
- regex: "^['\"`](\\.\\./|\\./|@/)"
29
- # NOTE: `$OPTS` is only bound by the two-argument pattern arms above. This
30
- # `not` constraint filters out `{ ssr: false }` only for those arms; the
31
- # single-argument dynamic import arms still match and remain violations.
32
- OPTS:
33
- not:
34
- regex: '(?:\bssr\b|[''"]ssr[''"])\s*:\s*false\b'
35
- examples:
36
- - code: "const Comp = dynamic(() => import('./my-component'))"
37
- isValid: false
38
- file: 'web/app/page.tsx'
39
- - code: 'const Comp = dynamic(() => import(`./my-component`))'
40
- isValid: false
41
- file: 'web/app/page.tsx'
42
- - code: "const Comp = dynamic(() => import('../components/comp'))"
43
- isValid: false
44
- file: 'web/app/page.tsx'
45
- - code: "const Comp = dynamic(() => import('./add-tag-form').then(m => ({ default: m.AddTagForm })))"
46
- isValid: false
47
- file: 'web/app/page.tsx'
48
- - code: "const Comp = nextDynamic(() => import('./my-component'))"
49
- isValid: false
50
- file: 'web/app/page.tsx'
51
- - code: "const Comp = dynamic(() => import('./c'), { loading: () => null })"
52
- isValid: false
53
- file: 'web/app/page.tsx'
54
- - code: "const Comp = dynamic(() => import('./c'), { not_ssr: false })"
55
- isValid: false
56
- file: 'web/app/page.tsx'
57
- - code: "const Comp = dynamic(() => import('./c'), { ssr: falsey })"
58
- isValid: false
59
- file: 'web/app/page.tsx'
60
- - code: "const Comp = dynamic(() => import('./c'), { ssr: false })"
61
- isValid: true
62
- file: 'web/app/page.tsx'
63
- - code: 'const Comp = dynamic(() => import("./c"), { "ssr": false })'
64
- isValid: true
65
- file: 'web/app/page.tsx'
66
- - code: "const Comp = dynamic(() => import('./c'), { 'ssr': false })"
67
- isValid: true
68
- file: 'web/app/page.tsx'
69
- - code: "const Comp = lazy(() => import('./my-component'))"
70
- isValid: true
71
- file: 'web/app/page.tsx'
@@ -1,33 +0,0 @@
1
- # NOTE: ast-grep maps `.tsx` to language `Tsx`, not `TypeScript`. This rule
2
- # only scans `.tsx` files; see no-enable-indexscan-off.yml for the companion
3
- # rule that covers `.ts/.mts/.cts` files.
4
- id: no-enable-indexscan-off-tsx
5
- language: Tsx
6
- severity: error
7
- message: >-
8
- Do not use `enable_indexscan = off` — it disables index scans repo-wide within the transaction
9
- (not just for HNSW vector search), forcing a full sequential scan. For HNSW overfiltering on a
10
- small hard-capped result window, use `applyFilteredVectorScan()` from `@modules/search-utils`
11
- (pgvector `hnsw.iterative_scan`) instead.
12
- rule:
13
- any:
14
- - kind: string
15
- - kind: template_string
16
- regex: '(?i)enable_indexscan[\s\S]{0,12}(=|to)[\s\S]{0,12}off'
17
- examples:
18
- - code: |-
19
- import sql from 'sql-template-strings'
20
- export const query = sql`SET LOCAL enable_indexscan = off`
21
- isValid: false
22
- file: 'web/lib/__tests__/example.tsx'
23
- - code: |-
24
- import sql from 'sql-template-strings'
25
- export const query = sql`SET LOCAL enable_indexscan TO off`
26
- isValid: false
27
- file: 'web/lib/__tests__/example.tsx'
28
- - code: |-
29
- export function Example() {
30
- return <div>hnsw.iterative_scan = strict_order</div>
31
- }
32
- isValid: true
33
- file: 'web/components/example.tsx'
@@ -1,42 +0,0 @@
1
- # NOTE: `__tests__` directories must contain tests only. Shared helpers, registries,
2
- # fixtures, and other imported support modules belong in a nearby `test-helpers/`
3
- # directory so test roots stay easy to scan.
4
- # NOTE: ast-grep maps `.tsx` to language `Tsx`, not `TypeScript`. This rule
5
- # only scans `.tsx` files; see no-exports-in-tests-folder.yml for the companion
6
- # rule that covers `.ts/.mts/.cts` files.
7
- id: no-exports-in-tests-folder-tsx
8
- language: Tsx
9
- severity: error
10
- message: 'Do not export from files under __tests__; move shared test support to a test-helpers directory.'
11
- files:
12
- - '**/__tests__/**/*.tsx'
13
- rule:
14
- all:
15
- - kind: export_statement
16
- - not:
17
- pattern: export {}
18
- examples:
19
- - code: |
20
- export function renderThing() {
21
- return <div />
22
- }
23
- isValid: false
24
- file: 'web/components/example/__tests__/comment-tree-test-helpers.tsx'
25
- - code: 'export type RenderThing = () => JSX.Element'
26
- isValid: false
27
- file: 'web/components/__tests__/render-helpers.tsx'
28
- - code: |
29
- it('renders shell exports', () => {
30
- expect(`export NEXT_PORT=3901`).toContain('export')
31
- })
32
- isValid: true
33
- file: 'web/components/example/__tests__/comment-tree.test.tsx'
34
- - code: |
35
- function renderThing() {
36
- return <div />
37
- }
38
- isValid: true
39
- file: 'web/components/example/__tests__/comment-tree.test.tsx'
40
- - code: 'export {}'
41
- isValid: true
42
- file: 'web/components/example/__tests__/global-jsx.test.tsx'
@@ -1,43 +0,0 @@
1
- id: no-self-comparison-assertion-tsx
2
- language: Tsx
3
- severity: error
4
- message: 'This compares an expression against itself (e.g. expect(result.id).toBe(result.id)). ast-grep metavariables enforce identical text, so both sides read the same value the same way; nearly every real occurrence of this shape is a copy-paste bug where the second operand should have been an independently derived expected value. Compare against that instead, or a fixture-provided value the code under test did not produce. (Two syntactic shapes are exempted because they cannot be tautological by construction: an expression containing a call anywhere in it, e.g. expect(f(a)).toBe(f(a)) or expect((await load()).id).toBe((await load()).id), is a fresh invocation on each side and legitimately tests that the call is deterministic/pure; and a postfix/prefix increment or decrement, e.g. expect(index++).toBe(index++), advances state between the two reads. A getter or other stateful property access is NOT exempted -- ast-grep cannot see whether a given `.foo` is a plain field or a getter with side effects, and every real violation this rule has caught in this repo is a plain field, so this stays flagged by design; if you are deliberately asserting that such an accessor is stable across two reads, add an `ast-grep-ignore: no-self-comparison-assertion-tsx` comment with that rationale instead of weakening this rule.)'
5
- files:
6
- - '**/*.test.tsx'
7
- - '**/*.spec.tsx'
8
- rule:
9
- pattern: 'expect($X).toBe($X)'
10
- constraints:
11
- X:
12
- not:
13
- any:
14
- - kind: 'true'
15
- - kind: 'false'
16
- - kind: 'number'
17
- - kind: 'string'
18
- - kind: 'null'
19
- - kind: 'undefined'
20
- - kind: 'call_expression'
21
- - kind: 'update_expression'
22
- - has:
23
- kind: 'call_expression'
24
- stopBy: end
25
- examples:
26
- - code: 'expect(item.id).toBe(item.id)'
27
- isValid: false
28
- file: 'web/components/example/__tests__/add-to-list-menu-item.mock.test.tsx'
29
- - code: 'expect(item.id).toBe(expectedId)'
30
- isValid: true
31
- file: 'web/components/example/__tests__/add-to-list-menu-item.mock.test.tsx'
32
- - code: 'expect(formatItemLabel(item)).toBe(formatItemLabel(item))'
33
- isValid: true
34
- file: 'web/components/example/__tests__/add-to-list-menu-item.mock.test.tsx'
35
- - code: 'expect((await loadItem()).id).toBe((await loadItem()).id)'
36
- isValid: true
37
- file: 'web/components/example/__tests__/add-to-list-menu-item.mock.test.tsx'
38
- - code: 'expect(item.id).toBe(item.id)'
39
- isValid: true
40
- file: 'web/components/example/add-to-list-menu-item.tsx'
41
- - code: 'expect(index++).toBe(index++)'
42
- isValid: true
43
- file: 'web/components/example/__tests__/add-to-list-menu-item.mock.test.tsx'
@@ -1,31 +0,0 @@
1
- id: no-tautological-literal-assertion-tsx
2
- language: Tsx
3
- severity: error
4
- message: 'This asserts a literal against an identical literal (e.g. expect(true).toBe(true)), which can never fail and provides zero real coverage of the code under test. Assert on the actual value the code under test produced instead -- for example expect(await run()).not.toBeNull(), or a specific value or shape derived from calling the function being tested.'
5
- files:
6
- - '**/*.test.tsx'
7
- - '**/*.spec.tsx'
8
- rule:
9
- pattern: 'expect($LIT).toBe($LIT)'
10
- constraints:
11
- LIT:
12
- any:
13
- - kind: 'true'
14
- - kind: 'false'
15
- - kind: 'number'
16
- - kind: 'string'
17
- - kind: 'null'
18
- - kind: 'undefined'
19
- examples:
20
- - code: 'expect(true).toBe(true)'
21
- isValid: false
22
- file: 'web/components/example/__tests__/add-to-list-menu-item.mock.test.tsx'
23
- - code: "expect('placeholder').toBe('placeholder')"
24
- isValid: false
25
- file: 'web/components/example/__tests__/source-list-item.interactions.mock.test.tsx'
26
- - code: 'expect(screen.getByRole("button")).toBeInTheDocument()'
27
- isValid: true
28
- file: 'web/components/example/__tests__/add-to-list-menu-item.mock.test.tsx'
29
- - code: 'expect(true).toBe(true)'
30
- isValid: true
31
- file: 'web/components/example/add-to-list-menu-item.tsx'