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
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'
|