vouchington-tooling 0.6.2 → 0.6.3

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 (48) 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
package/README.md CHANGED
@@ -22,6 +22,7 @@ vouchington gha-runtime-audit --pr-workflow CI --push-workflow '/^Main CI \\(.+\
22
22
  vouchington require-up-to-date --remote origin --branch main
23
23
  vouchington gitleaks-directory-scan --config .gitleaks.toml
24
24
  vouchington ast-grep-examples --rules ast-grep-tests --config sgconfig.yml
25
+ vouchington ast-grep-pack
25
26
  vouchington gha-workspace-policy
26
27
  vouchington gha-output name
27
28
  vouchington gha-needs-results
@@ -88,6 +89,8 @@ ancestor of `HEAD`. `gitleaks-directory-scan` builds and scans isolated staged-i
88
89
  nonignored-working-tree mirrors with an explicit config; `--directory` selects the repository root.
89
90
  `ast-grep-examples` runs native `ast-grep test`, then validates each scoped rule's `files:` and
90
91
  `ignores:` examples with project `languageGlobs` replay from its root `--config`.
92
+ `ast-grep-pack` prints JSON `{ rules, config }` for the shipped unconditional rule pack. Point a
93
+ consumer `sgconfig.yml` `ruleDirs` at `rules` and keep product-specific YAML locally.
91
94
  `gha-workspace-policy` checks tracked workflow and composite-action files in the current repository;
92
95
  pass `--root`, `--workflow-directory`, or `--action-directory` for consumer-owned layouts.
93
96
 
@@ -192,6 +195,7 @@ import { checkGhaWorkspacePolicy } from 'vouchington-tooling/gha-workspace-polic
192
195
  import { requireUpToDate } from 'vouchington-tooling/require-up-to-date'
193
196
  import { runGitleaksDirectoryScan } from 'vouchington-tooling/gitleaks-directory-scan'
194
197
  import { runAstGrepExamples } from 'vouchington-tooling/ast-grep-examples'
198
+ import { astGrepPackPaths } from 'vouchington-tooling/ast-grep-pack'
195
199
  import { validateNugetUpdate } from 'vouchington-tooling/nuget-central-version'
196
200
  import { normalizeSwiftSource } from 'vouchington-tooling/swift-semantic-equal'
197
201
  import { parseUniqueSwiftBinaryTargetChecksum } from 'vouchington-tooling/swift-source-offset'
@@ -0,0 +1,44 @@
1
+ id: backend-no-test-file-imports
2
+ language: TypeScript
3
+ severity: error
4
+ message: 'Do not import backend test files from backend Vitest files; move shared helpers to test-helpers or a support module.'
5
+ files:
6
+ - 'backend/**/*.test.mts'
7
+ - 'backend/**/__tests__/**/*.mts'
8
+ rule:
9
+ any:
10
+ - all:
11
+ - kind: import_statement
12
+ - regex: "['\"`](?:\\.\\./|\\./)[^'\"`]*\\.test\\.mts['\"`]"
13
+ - all:
14
+ - kind: export_statement
15
+ - regex: "['\"`](?:\\.\\./|\\./)[^'\"`]*\\.test\\.mts['\"`]"
16
+ - pattern: 'import($PATH)'
17
+ regex: "['\"`](?:\\.\\./|\\./)[^'\"`]*\\.test\\.mts['\"`]"
18
+ - pattern: 'require($PATH)'
19
+ regex: "['\"`](?:\\.\\./|\\./)[^'\"`]*\\.test\\.mts['\"`]"
20
+ examples:
21
+ - code: "import { createTestUser } from './user.test.mts'"
22
+ isValid: false
23
+ file: 'backend/services/example/__tests__/user-login.test.mts'
24
+ - code: "import * as suite from './search.part-2.test.mts'"
25
+ isValid: false
26
+ file: 'backend/services/example/__tests__/search.test.mts'
27
+ - code: "import type { Helper } from '../helpers.test.mts'"
28
+ isValid: false
29
+ file: 'backend/services/example/__tests__/search.test.mts'
30
+ - code: "export { createThing } from './thing.test.mts'"
31
+ isValid: false
32
+ file: 'backend/services/example/__tests__/search.test.mts'
33
+ - code: "const mod = await import('./search.test.mts')"
34
+ isValid: false
35
+ file: 'backend/services/example/__tests__/search.test.mts'
36
+ - code: "const mod = require('./search.test.mts')"
37
+ isValid: false
38
+ file: 'backend/services/example/__tests__/search.test.mts'
39
+ - code: "import { searchSuite } from '../search.test-suites.mts'"
40
+ isValid: true
41
+ file: 'backend/services/example/__tests__/search.test.mts'
42
+ - code: "const mod = await import('../search.test-suites.mts')"
43
+ isValid: true
44
+ file: 'backend/services/example/__tests__/search.test.mts'
@@ -0,0 +1,57 @@
1
+ id: force-dynamic-layout
2
+ language: Tsx
3
+ severity: error
4
+ message: "layout file accesses request context but is missing `export const dynamic = 'force-dynamic'`"
5
+ files:
6
+ - 'web/app/**/layout.tsx'
7
+ ignores:
8
+ - 'web/app/layout.tsx'
9
+ rule:
10
+ all:
11
+ - kind: program
12
+ - has:
13
+ stopBy: end
14
+ any:
15
+ - pattern: 'headers()'
16
+ - pattern: 'cookies()'
17
+ - pattern: 'getCurrentUser($$$)'
18
+ - not:
19
+ has:
20
+ # No stopBy:end — only check direct children of program so that a
21
+ # 'use client' string nested inside a function body does not exempt
22
+ # the layout. Top-level directives and exports are direct children.
23
+ any:
24
+ - kind: expression_statement
25
+ has:
26
+ any:
27
+ - pattern: "'use client'"
28
+ - pattern: '"use client"'
29
+ - pattern: "export const dynamic = 'force-dynamic'"
30
+ examples:
31
+ - code: |
32
+ import { headers } from 'next/headers'
33
+ export const dynamic = 'force-dynamic'
34
+ export default function Layout({ children }) { return <div>{children}</div> }
35
+ isValid: true
36
+ file: 'web/app/dashboard/layout.tsx'
37
+ - code: |
38
+ import { headers } from 'next/headers'
39
+ export default function Layout({ children }) {
40
+ const h = headers()
41
+ return <div>{children}</div>
42
+ }
43
+ isValid: false
44
+ file: 'web/app/dashboard/layout.tsx'
45
+ - code: |
46
+ export default function Layout({ children }) { return <div>{children}</div> }
47
+ isValid: true
48
+ file: 'web/app/dashboard/layout.tsx'
49
+ - code: |
50
+ 'use client'
51
+ import { headers } from 'next/headers'
52
+ export default function Layout({ children }) {
53
+ const h = headers()
54
+ return <div>{children}</div>
55
+ }
56
+ isValid: true
57
+ file: 'web/app/dashboard/layout.tsx'
@@ -0,0 +1,38 @@
1
+ id: force-dynamic-pages
2
+ language: Tsx
3
+ severity: error
4
+ message: "page file must export `export const dynamic = 'force-dynamic'`"
5
+ files:
6
+ - 'web/app/**/page.tsx'
7
+ rule:
8
+ all:
9
+ - kind: program
10
+ - not:
11
+ has:
12
+ any:
13
+ - kind: expression_statement
14
+ has:
15
+ any:
16
+ - pattern: "'use client'"
17
+ - pattern: '"use client"'
18
+ - pattern: "export const dynamic = 'force-dynamic'"
19
+ examples:
20
+ - code: |
21
+ export const dynamic = 'force-dynamic'
22
+ export default function Page() { return <div /> }
23
+ isValid: true
24
+ file: 'web/app/home/page.tsx'
25
+ - code: |
26
+ 'use client'
27
+ export default function Page() { return <div /> }
28
+ isValid: true
29
+ file: 'web/app/home/page.tsx'
30
+ - code: |
31
+ "use client"
32
+ export default function Page() { return <div /> }
33
+ isValid: true
34
+ file: 'web/app/home/page.tsx'
35
+ - code: |
36
+ export default function Page() { return <div /> }
37
+ isValid: false
38
+ file: 'web/app/home/page.tsx'
@@ -0,0 +1,30 @@
1
+ id: no-dynamic-import-in-vitest-config
2
+ language: TypeScript
3
+ severity: error
4
+ message: >-
5
+ vitest config files must use static imports — dynamic import() prevents
6
+ vitest-project-mapping from statically analyzing project structure
7
+ files:
8
+ - '**/vitest.config.mts'
9
+ - '**/test-helpers/vitest-config/*.mts'
10
+ rule:
11
+ pattern: import($$$)
12
+ examples:
13
+ - code: "const { default: react } = await import('@vitejs/plugin-react')"
14
+ isValid: false
15
+ file: 'vitest.config.mts'
16
+ - code: "const [{ defineConfig }] = await Promise.all([import('vitest/config')])"
17
+ isValid: false
18
+ file: 'vitest.config.mts'
19
+ - code: "import react from '@vitejs/plugin-react'"
20
+ isValid: true
21
+ file: 'vitest.config.mts'
22
+ - code: "import { defineConfig } from 'vitest/config'"
23
+ isValid: true
24
+ file: 'vitest.config.mts'
25
+ - code: "const { default: react } = await import('@vitejs/plugin-react')"
26
+ isValid: false
27
+ file: 'test-helpers/vitest-config/backend-core-projects.mts'
28
+ - code: "import react from '@vitejs/plugin-react'"
29
+ isValid: true
30
+ file: 'test-helpers/vitest-config/backend-core-projects.mts'
@@ -0,0 +1,71 @@
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'
@@ -0,0 +1,71 @@
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
5
+ language: TypeScript
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 `.ts` files; see no-dynamic-server-components-tsx.yml for the
10
+ # companion rule that covers `.tsx` files.
11
+ files:
12
+ - 'web/**/*.ts'
13
+ ignores:
14
+ - '**/*.test.ts'
15
+ - '**/*.spec.ts'
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.ts'
39
+ - code: 'const Comp = dynamic(() => import(`./my-component`))'
40
+ isValid: false
41
+ file: 'web/app/page.ts'
42
+ - code: "const Comp = dynamic(() => import('../components/comp'))"
43
+ isValid: false
44
+ file: 'web/app/page.ts'
45
+ - code: "const Comp = dynamic(() => import('./add-tag-form').then(m => ({ default: m.AddTagForm })))"
46
+ isValid: false
47
+ file: 'web/app/page.ts'
48
+ - code: "const Comp = nextDynamic(() => import('./my-component'))"
49
+ isValid: false
50
+ file: 'web/app/page.ts'
51
+ - code: "const Comp = dynamic(() => import('./c'), { loading: () => null })"
52
+ isValid: false
53
+ file: 'web/app/page.ts'
54
+ - code: "const Comp = dynamic(() => import('./c'), { not_ssr: false })"
55
+ isValid: false
56
+ file: 'web/app/page.ts'
57
+ - code: "const Comp = dynamic(() => import('./c'), { ssr: falsey })"
58
+ isValid: false
59
+ file: 'web/app/page.ts'
60
+ - code: "const Comp = dynamic(() => import('./c'), { ssr: false })"
61
+ isValid: true
62
+ file: 'web/app/page.ts'
63
+ - code: 'const Comp = dynamic(() => import("./c"), { "ssr": false })'
64
+ isValid: true
65
+ file: 'web/app/page.ts'
66
+ - code: "const Comp = dynamic(() => import('./c'), { 'ssr': false })"
67
+ isValid: true
68
+ file: 'web/app/page.ts'
69
+ - code: "const Comp = lazy(() => import('./my-component'))"
70
+ isValid: true
71
+ file: 'web/app/page.ts'
@@ -0,0 +1,33 @@
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'
@@ -0,0 +1,41 @@
1
+ # NOTE: ast-grep maps `.tsx` to language `Tsx`, not `TypeScript`. This rule
2
+ # only scans `.ts/.mts/.cts` files; see no-enable-indexscan-off-tsx.yml for
3
+ # the companion rule that covers `.tsx` files.
4
+ id: no-enable-indexscan-off
5
+ language: TypeScript
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: 'backend/services/example/tools/semantic.mts'
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: 'backend/services/example/tools/semantic.mts'
28
+ - code: |-
29
+ const guard = "current_setting('enable_indexscan') = 'off'"
30
+ isValid: false
31
+ file: 'backend/test-helpers/entities/posts-semantic.test.mts'
32
+ - code: |-
33
+ import sql from 'sql-template-strings'
34
+ export const query = sql`SET LOCAL hnsw.iterative_scan = strict_order`
35
+ isValid: true
36
+ file: 'backend/services/example/tools/semantic.mts'
37
+ - code: |-
38
+ // Do not disable index scans for vector search; use iterative scans instead.
39
+ const note = 'see applyFilteredVectorScan'
40
+ isValid: true
41
+ file: 'backend/src/vector-scan.mts'
@@ -0,0 +1,42 @@
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'
@@ -0,0 +1,43 @@
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 `.ts/.mts/.cts` files; see no-exports-in-tests-folder-tsx.yml for
6
+ # the companion rule that covers `.tsx` files.
7
+ id: no-exports-in-tests-folder
8
+ language: TypeScript
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__/**/*.ts'
13
+ - '**/__tests__/**/*.mts'
14
+ - '**/__tests__/**/*.cts'
15
+ rule:
16
+ all:
17
+ - kind: export_statement
18
+ - not:
19
+ pattern: export {}
20
+ examples:
21
+ - code: 'export const makeThing = () => ({})'
22
+ isValid: false
23
+ file: 'backend/services/example/__tests__/user-helpers.mts'
24
+ - code: 'export type Helper = { id: string }'
25
+ isValid: false
26
+ file: 'web/lib/__tests__/manifest-registry.ts'
27
+ - code: "export { makeThing } from './thing'"
28
+ isValid: false
29
+ file: 'src/__tests__/helpers.mts'
30
+ - code: |
31
+ describe('script rendering', () => {
32
+ it('renders shell exports', () => {
33
+ expect(`export NEXT_PORT=3901`).toContain('export')
34
+ })
35
+ })
36
+ isValid: true
37
+ file: 'src/__tests__/stop-services.test.mts'
38
+ - code: 'const makeThing = () => ({})'
39
+ isValid: true
40
+ file: 'backend/services/example/__tests__/users.test.mts'
41
+ - code: 'export {}'
42
+ isValid: true
43
+ file: 'backend/services/example/__tests__/global-script.test.mts'
@@ -0,0 +1,31 @@
1
+ id: no-object-assign-new-error
2
+ language: TypeScript
3
+ severity: error
4
+ message: 'Use http-errors/http-assert or an explicit typed error helper instead of Object.assign(new Error(...))'
5
+ files:
6
+ - '**/*.mts'
7
+ - '**/*.ts'
8
+ - '**/*.tsx'
9
+ ignores:
10
+ - '**/*.test.mts'
11
+ - '**/*.test.ts'
12
+ - '**/*.test.tsx'
13
+ - '**/*.mock.test.mts'
14
+ - '**/*.mock.test.ts'
15
+ - '**/*.mock.test.tsx'
16
+ - '**/*.spec.mts'
17
+ - '**/*.spec.ts'
18
+ - '**/*.spec.tsx'
19
+ - 'web/storybook/**'
20
+ rule:
21
+ pattern: Object.assign(new Error($$$), $$$)
22
+ examples:
23
+ - code: "throw Object.assign(new Error('missing'), { status: 404 })"
24
+ isValid: false
25
+ file: backend/services/example.mts
26
+ - code: "throw createHttpError(404, 'missing')"
27
+ isValid: true
28
+ file: backend/services/example.mts
29
+ - code: "const error = new Error('suppressed'); error.tags = { suppressLogging: true }"
30
+ isValid: true
31
+ file: backend/test-helpers/suppressed-error.mts
@@ -0,0 +1,33 @@
1
+ id: no-runner-temp-nullish
2
+ language: TypeScript
3
+ severity: error
4
+ message: 'Use logical OR for RUNNER_TEMP temp roots so an empty string falls back safely.'
5
+ files:
6
+ - '**/*.ts'
7
+ - '**/*.tsx'
8
+ - '**/*.mts'
9
+ - '**/*.cts'
10
+ rule:
11
+ any:
12
+ - pattern: $ENV.RUNNER_TEMP ?? $$$
13
+ - pattern: $ENV['RUNNER_TEMP'] ?? $$$
14
+ - pattern: $ENV["RUNNER_TEMP"] ?? $$$
15
+ examples:
16
+ - code: 'const root = process.env.RUNNER_TEMP ?? os.tmpdir()'
17
+ isValid: false
18
+ file: 'src/setup.mts'
19
+ - code: 'const root = env.RUNNER_TEMP ?? env.TMPDIR'
20
+ isValid: false
21
+ file: 'src/env.mts'
22
+ - code: 'const root = process.env["RUNNER_TEMP"] ?? os.tmpdir()'
23
+ isValid: false
24
+ file: 'src/setup.mts'
25
+ - code: "const root = env['RUNNER_TEMP'] ?? env.TMPDIR"
26
+ isValid: false
27
+ file: 'src/env.mts'
28
+ - code: 'const root = process.env.RUNNER_TEMP || os.tmpdir()'
29
+ isValid: true
30
+ file: 'src/setup.mts'
31
+ - code: 'const root = env.RUNNER_TEMP || env.TMPDIR || tmpdir()'
32
+ isValid: true
33
+ file: 'src/env.mts'
@@ -0,0 +1,43 @@
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'