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,53 @@
|
|
|
1
|
+
id: no-self-comparison-assertion
|
|
2
|
+
language: TypeScript
|
|
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` comment with that rationale instead of weakening this rule.)'
|
|
5
|
+
files:
|
|
6
|
+
- '**/*.test.ts'
|
|
7
|
+
- '**/*.test.mts'
|
|
8
|
+
- '**/*.test.cts'
|
|
9
|
+
- '**/*.spec.ts'
|
|
10
|
+
- '**/*.spec.mts'
|
|
11
|
+
- '**/*.spec.cts'
|
|
12
|
+
rule:
|
|
13
|
+
pattern: 'expect($X).toBe($X)'
|
|
14
|
+
constraints:
|
|
15
|
+
X:
|
|
16
|
+
not:
|
|
17
|
+
any:
|
|
18
|
+
- kind: 'true'
|
|
19
|
+
- kind: 'false'
|
|
20
|
+
- kind: 'number'
|
|
21
|
+
- kind: 'string'
|
|
22
|
+
- kind: 'null'
|
|
23
|
+
- kind: 'undefined'
|
|
24
|
+
- kind: 'call_expression'
|
|
25
|
+
- kind: 'update_expression'
|
|
26
|
+
- has:
|
|
27
|
+
kind: 'call_expression'
|
|
28
|
+
stopBy: end
|
|
29
|
+
examples:
|
|
30
|
+
- code: 'expect(result.id).toBe(result.id)'
|
|
31
|
+
isValid: false
|
|
32
|
+
file: 'backend/services/example/__tests__/items.test.mts'
|
|
33
|
+
- code: 'expect(conversationId).toBe(conversationId)'
|
|
34
|
+
isValid: false
|
|
35
|
+
file: 'src/__tests__/routes.user-admin.part-5.test.mts'
|
|
36
|
+
- code: 'expect(result.id).toBe(expectedId)'
|
|
37
|
+
isValid: true
|
|
38
|
+
file: 'backend/services/example/__tests__/items.test.mts'
|
|
39
|
+
- code: 'expect(await run()).not.toBeNull()'
|
|
40
|
+
isValid: true
|
|
41
|
+
file: 'src/__tests__/routes.user-admin.part-5.test.mts'
|
|
42
|
+
- code: 'expect(buildCanonicalRequestString(...args)).toBe(buildCanonicalRequestString(...args))'
|
|
43
|
+
isValid: true
|
|
44
|
+
file: 'backend/services/example/request-canonical.test.mts'
|
|
45
|
+
- code: 'expect((await load()).id).toBe((await load()).id)'
|
|
46
|
+
isValid: true
|
|
47
|
+
file: 'backend/services/example/__tests__/items.test.mts'
|
|
48
|
+
- code: 'expect(result.id).toBe(result.id)'
|
|
49
|
+
isValid: true
|
|
50
|
+
file: 'backend/services/example/items.mts'
|
|
51
|
+
- code: 'expect(index++).toBe(index++)'
|
|
52
|
+
isValid: true
|
|
53
|
+
file: 'backend/services/example/__tests__/items.test.mts'
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
id: no-sql-offset
|
|
2
|
+
language: TypeScript
|
|
3
|
+
severity: error
|
|
4
|
+
message: 'Do not use SQL OFFSET in backend sql templates; use cursor pagination, LIMIT + 1, COUNT, EXISTS, or ROW_NUMBER() instead'
|
|
5
|
+
files:
|
|
6
|
+
- 'backend/**/*.ts'
|
|
7
|
+
- 'backend/**/*.mts'
|
|
8
|
+
- 'backend/**/*.cts'
|
|
9
|
+
rule:
|
|
10
|
+
all:
|
|
11
|
+
- kind: template_string
|
|
12
|
+
# This repo's SQL templates parameterize with `${...}`; numeric literals catch raw
|
|
13
|
+
# examples and fixtures without flagging ordinary prose such as "offset by".
|
|
14
|
+
- regex: '(?i)\bOFFSET\s+(?:\$\{|[0-9])'
|
|
15
|
+
- inside:
|
|
16
|
+
kind: call_expression
|
|
17
|
+
stopBy: end
|
|
18
|
+
has:
|
|
19
|
+
kind: identifier
|
|
20
|
+
regex: '^sql$'
|
|
21
|
+
stopBy: neighbor
|
|
22
|
+
examples:
|
|
23
|
+
- code: |-
|
|
24
|
+
import sql from 'sql-template-strings'
|
|
25
|
+
export const query = sql`
|
|
26
|
+
SELECT id FROM posts
|
|
27
|
+
ORDER BY id DESC
|
|
28
|
+
OFFSET ${limit}
|
|
29
|
+
`
|
|
30
|
+
isValid: false
|
|
31
|
+
file: 'backend/services/example/search.mts'
|
|
32
|
+
- code: |-
|
|
33
|
+
import sql from 'sql-template-strings'
|
|
34
|
+
export const query = sql`
|
|
35
|
+
SELECT id FROM posts
|
|
36
|
+
ORDER BY id DESC
|
|
37
|
+
OFFSET 10
|
|
38
|
+
`
|
|
39
|
+
isValid: false
|
|
40
|
+
file: 'backend/services/example/search.mts'
|
|
41
|
+
- code: |-
|
|
42
|
+
import sql from 'sql-template-strings'
|
|
43
|
+
export const query = sql`
|
|
44
|
+
SELECT id FROM posts
|
|
45
|
+
ORDER BY id DESC
|
|
46
|
+
LIMIT ${limit + 1}
|
|
47
|
+
`
|
|
48
|
+
isValid: true
|
|
49
|
+
file: 'backend/services/example/search.mts'
|
|
50
|
+
- code: |-
|
|
51
|
+
export function copyBuffer(chunks: Uint8Array[]) {
|
|
52
|
+
let offset = 0
|
|
53
|
+
for (const chunk of chunks) offset += chunk.byteLength
|
|
54
|
+
return offset
|
|
55
|
+
}
|
|
56
|
+
isValid: true
|
|
57
|
+
file: 'backend/services/example/domain-verification-well-known.mts'
|
|
58
|
+
- code: |-
|
|
59
|
+
import sql from 'sql-template-strings'
|
|
60
|
+
export const query = sql`
|
|
61
|
+
INSERT INTO examples (body)
|
|
62
|
+
VALUES ('offset by a travel credit')
|
|
63
|
+
`
|
|
64
|
+
isValid: true
|
|
65
|
+
file: 'backend/src/example.mts'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
id: no-sync-throw-assert-on-async-enqueue
|
|
2
|
+
language: TypeScript
|
|
3
|
+
severity: error
|
|
4
|
+
message: 'Best-effort syntactic heuristic (not type-aware): this wraps a synchronous throw-assertion around a call that looks like a Promise-returning enqueue call. expect(() => fn()).not.toThrow() and .toThrow() only observe a synchronous throw and never see an async rejection, so this gives false confidence in the async contract. Await the call and assert on its resolved value instead, e.g. expect(await enqueueFoo()).toBeUndefined() or await expect(enqueueFoo()).resolves.toBeDefined().'
|
|
5
|
+
files:
|
|
6
|
+
- 'backend/**/*.test.mts'
|
|
7
|
+
rule:
|
|
8
|
+
# NOTE: member-call patterns MUST precede plain-call patterns in this list. ast-grep's `any:`
|
|
9
|
+
# does not backtrack across constraint failures: once an earlier arm structurally matches a node
|
|
10
|
+
# (even loosely, e.g. a bare-identifier pattern absorbing a qualified member-expression) and its
|
|
11
|
+
# constraint then fails, the match for that node is rejected outright rather than falling through
|
|
12
|
+
# to try a later, more specific arm. Listing the more specific member-call shape first avoids this.
|
|
13
|
+
any:
|
|
14
|
+
- pattern: 'expect(() => $OBJ.$CALL($$$ARGS)).not.toThrow()'
|
|
15
|
+
- pattern: 'expect(() => $OBJ.$CALL($$$ARGS)).toThrow()'
|
|
16
|
+
- pattern: 'expect(() => $CALL($$$ARGS)).not.toThrow()'
|
|
17
|
+
- pattern: 'expect(() => $CALL($$$ARGS)).toThrow()'
|
|
18
|
+
constraints:
|
|
19
|
+
CALL:
|
|
20
|
+
all:
|
|
21
|
+
- regex: '^enqueue\w*$'
|
|
22
|
+
- not:
|
|
23
|
+
regex: 'BestEffort'
|
|
24
|
+
examples:
|
|
25
|
+
- code: 'expect(() => enqueueBulkSourceSyncs([])).not.toThrow()'
|
|
26
|
+
isValid: false
|
|
27
|
+
file: 'backend/queues/example/enqueues.generated.test.mts'
|
|
28
|
+
- code: 'expect(() => queueModule.enqueueBulkSourceSyncs([])).not.toThrow()'
|
|
29
|
+
isValid: false
|
|
30
|
+
file: 'backend/queues/example/enqueues.test.mts'
|
|
31
|
+
- code: "expect(() => assertSafeUrlSync('http://example.com/path')).not.toThrow()"
|
|
32
|
+
isValid: true
|
|
33
|
+
file: 'backend/services/example/ssrf.test.mts'
|
|
34
|
+
- code: 'expect(await enqueueBulkSourceSyncs([])).toBeUndefined()'
|
|
35
|
+
isValid: true
|
|
36
|
+
file: 'backend/queues/example/enqueues.generated.test.mts'
|
|
37
|
+
- code: 'expect(() => enqueueBulkSourceSyncs([])).not.toThrow()'
|
|
38
|
+
isValid: true
|
|
39
|
+
file: 'backend/queues/example/enqueues.mts'
|
|
40
|
+
- code: "expect(() => enqueueReportResolutionNotificationsBestEffort([{ user_id: crypto.randomUUID(), id: crypto.randomUUID() }], () => { throw new Error('queue unavailable') })).not.toThrow()"
|
|
41
|
+
isValid: true
|
|
42
|
+
file: 'backend/services/example/__tests__/resolve.test.mts'
|
|
@@ -0,0 +1,31 @@
|
|
|
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'
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
id: no-tautological-literal-assertion
|
|
2
|
+
language: TypeScript
|
|
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.ts'
|
|
7
|
+
- '**/*.test.mts'
|
|
8
|
+
- '**/*.test.cts'
|
|
9
|
+
- '**/*.spec.ts'
|
|
10
|
+
- '**/*.spec.mts'
|
|
11
|
+
- '**/*.spec.cts'
|
|
12
|
+
rule:
|
|
13
|
+
pattern: 'expect($LIT).toBe($LIT)'
|
|
14
|
+
constraints:
|
|
15
|
+
LIT:
|
|
16
|
+
any:
|
|
17
|
+
- kind: 'true'
|
|
18
|
+
- kind: 'false'
|
|
19
|
+
- kind: 'number'
|
|
20
|
+
- kind: 'string'
|
|
21
|
+
- kind: 'null'
|
|
22
|
+
- kind: 'undefined'
|
|
23
|
+
examples:
|
|
24
|
+
- code: 'expect(true).toBe(true)'
|
|
25
|
+
isValid: false
|
|
26
|
+
file: 'src/__tests__/client-content-routes.test.mts'
|
|
27
|
+
- code: "expect('placeholder').toBe('placeholder')"
|
|
28
|
+
isValid: false
|
|
29
|
+
file: 'backend/services/example/__tests__/items.test.mts'
|
|
30
|
+
- code: 'expect(await run()).not.toBeNull()'
|
|
31
|
+
isValid: true
|
|
32
|
+
file: 'src/__tests__/client-content-routes.test.mts'
|
|
33
|
+
- code: 'expect(page2.results.length).toBeLessThanOrEqual(2)'
|
|
34
|
+
isValid: true
|
|
35
|
+
file: 'backend/services/example/get-trending-posts.test.mts'
|
|
36
|
+
- code: 'expect(true).toBe(true)'
|
|
37
|
+
isValid: true
|
|
38
|
+
file: 'backend/services/example/get-trending-posts.mts'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# NOTE: This rule flags eslint/oxlint disable directives that suppress max-lines
|
|
2
|
+
# (or all rules) inside test files. Test files must stay within their line limit;
|
|
3
|
+
# split the test or extract reusable helpers instead.
|
|
4
|
+
# NOTE: ast-grep maps `.tsx` to language `Tsx`, not `TypeScript`. This rule
|
|
5
|
+
# only scans `.tsx` files; see no-test-max-lines-disable.yml for the
|
|
6
|
+
# companion rule that covers `.ts/.mts/.cts` files.
|
|
7
|
+
id: no-test-max-lines-disable-tsx
|
|
8
|
+
language: Tsx
|
|
9
|
+
severity: error
|
|
10
|
+
message: 'Test files must not disable max-lines or all lint rules; split the test or extract reusable helpers instead.'
|
|
11
|
+
files:
|
|
12
|
+
- '**/*.test.tsx'
|
|
13
|
+
- '**/*.spec.tsx'
|
|
14
|
+
- '**/__tests__/**/*.tsx'
|
|
15
|
+
rule:
|
|
16
|
+
kind: comment
|
|
17
|
+
any:
|
|
18
|
+
- regex: '^(?://|/\*)\s*(?:eslint|oxlint)-disable(?:-next-line|-line)?\s*(?:\*/\s*$|--|\s*$)'
|
|
19
|
+
- regex: '^(?://|/\*)\s*(?:eslint|oxlint)-disable(?:-next-line|-line)?(?:[^\n-]|-[^\n-])*(?:\s|,)(?:max-lines(?:[^-a-zA-Z0-9_]|$)|eslint/max-lines(?:[^-a-zA-Z0-9_]|$))'
|
|
20
|
+
examples:
|
|
21
|
+
- code: '// oxlint-disable max-lines'
|
|
22
|
+
isValid: false
|
|
23
|
+
file: 'web/components/__tests__/foo.test.tsx'
|
|
24
|
+
- code: '// eslint-disable'
|
|
25
|
+
isValid: false
|
|
26
|
+
file: 'web/components/__tests__/foo.test.tsx'
|
|
27
|
+
- code: '// oxlint-disable vitest/expect-expect'
|
|
28
|
+
isValid: true
|
|
29
|
+
file: 'web/components/__tests__/foo.test.tsx'
|
|
30
|
+
- code: '// oxlint-disable no-console'
|
|
31
|
+
isValid: true
|
|
32
|
+
file: 'web/components/__tests__/foo.spec.tsx'
|
|
33
|
+
- code: '// eslint-disable max-lines-per-function'
|
|
34
|
+
isValid: true
|
|
35
|
+
file: 'web/components/__tests__/foo.test.tsx'
|
|
36
|
+
- code: '// eslint-disable eslint/max-lines-per-function'
|
|
37
|
+
isValid: true
|
|
38
|
+
file: 'web/components/__tests__/foo.test.tsx'
|
|
39
|
+
- code: '// oxlint-disable no-console -- split because of max-lines'
|
|
40
|
+
isValid: true
|
|
41
|
+
file: 'web/components/__tests__/foo.test.tsx'
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# NOTE: This rule flags eslint/oxlint disable directives that suppress max-lines
|
|
2
|
+
# (or all rules) inside test files. Test files must stay within their line limit;
|
|
3
|
+
# split the test or extract reusable helpers instead.
|
|
4
|
+
# NOTE: ast-grep maps `.tsx` to language `Tsx`, not `TypeScript`. This rule
|
|
5
|
+
# only scans `.ts/.mts/.cts` files; see no-test-max-lines-disable-tsx.yml for
|
|
6
|
+
# the companion rule that covers `.tsx` files.
|
|
7
|
+
id: no-test-max-lines-disable
|
|
8
|
+
language: TypeScript
|
|
9
|
+
severity: error
|
|
10
|
+
message: 'Test files must not disable max-lines or all lint rules; split the test or extract reusable helpers instead.'
|
|
11
|
+
files:
|
|
12
|
+
- '**/*.test.ts'
|
|
13
|
+
- '**/*.test.mts'
|
|
14
|
+
- '**/*.test.cts'
|
|
15
|
+
- '**/*.spec.ts'
|
|
16
|
+
- '**/*.spec.mts'
|
|
17
|
+
- '**/*.spec.cts'
|
|
18
|
+
- '**/__tests__/**/*.ts'
|
|
19
|
+
- '**/__tests__/**/*.mts'
|
|
20
|
+
- '**/__tests__/**/*.cts'
|
|
21
|
+
rule:
|
|
22
|
+
kind: comment
|
|
23
|
+
any:
|
|
24
|
+
- regex: '^(?://|/\*)\s*(?:eslint|oxlint)-disable(?:-next-line|-line)?\s*(?:\*/\s*$|--|\s*$)'
|
|
25
|
+
- regex: '^(?://|/\*)\s*(?:eslint|oxlint)-disable(?:-next-line|-line)?(?:[^\n-]|-[^\n-])*(?:\s|,)(?:max-lines(?:[^-a-zA-Z0-9_]|$)|eslint/max-lines(?:[^-a-zA-Z0-9_]|$))'
|
|
26
|
+
examples:
|
|
27
|
+
- code: '// oxlint-disable max-lines'
|
|
28
|
+
isValid: false
|
|
29
|
+
file: 'backend/services/users.test.ts'
|
|
30
|
+
- code: '// eslint-disable'
|
|
31
|
+
isValid: false
|
|
32
|
+
file: 'backend/services/users.test.mts'
|
|
33
|
+
- code: '// oxlint-disable-next-line max-lines'
|
|
34
|
+
isValid: false
|
|
35
|
+
file: 'backend/services/users.spec.ts'
|
|
36
|
+
- code: '/* eslint-disable eslint/max-lines */'
|
|
37
|
+
isValid: false
|
|
38
|
+
file: 'backend/__tests__/foo.ts'
|
|
39
|
+
- code: '// oxlint-disable vitest/expect-expect'
|
|
40
|
+
isValid: true
|
|
41
|
+
file: 'backend/services/users.test.ts'
|
|
42
|
+
- code: '// oxlint-disable no-console'
|
|
43
|
+
isValid: true
|
|
44
|
+
file: 'backend/services/users.test.mts'
|
|
45
|
+
- code: '// eslint-disable max-lines-per-function'
|
|
46
|
+
isValid: true
|
|
47
|
+
file: 'backend/services/users.test.ts'
|
|
48
|
+
- code: '// eslint-disable eslint/max-lines-per-function'
|
|
49
|
+
isValid: true
|
|
50
|
+
file: 'backend/services/users.test.ts'
|
|
51
|
+
- code: '// oxlint-disable no-console -- split because of max-lines'
|
|
52
|
+
isValid: true
|
|
53
|
+
file: 'backend/services/users.test.ts'
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
id: no-three-sequential-awaits-tsx
|
|
2
|
+
language: Tsx
|
|
3
|
+
severity: error
|
|
4
|
+
message: 'Avoid 3 sequential await statements; abstract dependent work or parallelize independent work with Promise.all().'
|
|
5
|
+
files:
|
|
6
|
+
- '**/*.tsx'
|
|
7
|
+
ignores:
|
|
8
|
+
- '**/*.test.tsx'
|
|
9
|
+
- '**/*.spec.tsx'
|
|
10
|
+
- '**/*.mock.test.tsx'
|
|
11
|
+
- '**/*.stories.tsx'
|
|
12
|
+
- 'web/test-helpers/**'
|
|
13
|
+
rule:
|
|
14
|
+
all:
|
|
15
|
+
- not:
|
|
16
|
+
follows:
|
|
17
|
+
stopBy: neighbor
|
|
18
|
+
any:
|
|
19
|
+
- all:
|
|
20
|
+
- pattern:
|
|
21
|
+
context: await $P
|
|
22
|
+
selector: expression_statement
|
|
23
|
+
- not:
|
|
24
|
+
pattern: await params
|
|
25
|
+
- not:
|
|
26
|
+
pattern: await searchParams
|
|
27
|
+
- not:
|
|
28
|
+
has:
|
|
29
|
+
pattern: Promise.all($$)
|
|
30
|
+
- not:
|
|
31
|
+
has:
|
|
32
|
+
pattern: Promise.allSettled($$)
|
|
33
|
+
- all:
|
|
34
|
+
- pattern: const $W = await $P
|
|
35
|
+
- not:
|
|
36
|
+
pattern: const $W = await params
|
|
37
|
+
- not:
|
|
38
|
+
pattern: const $W = await searchParams
|
|
39
|
+
- not:
|
|
40
|
+
pattern: const $W = await Promise.all($$$)
|
|
41
|
+
- not:
|
|
42
|
+
pattern: const $W = await Promise.allSettled($$$)
|
|
43
|
+
- all:
|
|
44
|
+
- pattern: let $W = await $P
|
|
45
|
+
- not:
|
|
46
|
+
pattern: let $W = await params
|
|
47
|
+
- not:
|
|
48
|
+
pattern: let $W = await searchParams
|
|
49
|
+
- not:
|
|
50
|
+
pattern: let $W = await Promise.all($$$)
|
|
51
|
+
- not:
|
|
52
|
+
pattern: let $W = await Promise.allSettled($$$)
|
|
53
|
+
- all:
|
|
54
|
+
- pattern: var $W = await $P
|
|
55
|
+
- not:
|
|
56
|
+
pattern: var $W = await params
|
|
57
|
+
- not:
|
|
58
|
+
pattern: var $W = await searchParams
|
|
59
|
+
- not:
|
|
60
|
+
pattern: var $W = await Promise.all($$$)
|
|
61
|
+
- not:
|
|
62
|
+
pattern: var $W = await Promise.allSettled($$$)
|
|
63
|
+
- all:
|
|
64
|
+
- pattern:
|
|
65
|
+
context: $W = await $P
|
|
66
|
+
selector: expression_statement
|
|
67
|
+
- not:
|
|
68
|
+
pattern: $W = await params
|
|
69
|
+
- not:
|
|
70
|
+
pattern: $W = await searchParams
|
|
71
|
+
- not:
|
|
72
|
+
pattern: $W = await Promise.all($$$)
|
|
73
|
+
- not:
|
|
74
|
+
pattern: $W = await Promise.allSettled($$$)
|
|
75
|
+
- any:
|
|
76
|
+
- all:
|
|
77
|
+
- pattern:
|
|
78
|
+
context: await $A
|
|
79
|
+
selector: expression_statement
|
|
80
|
+
- not:
|
|
81
|
+
pattern: await params
|
|
82
|
+
- not:
|
|
83
|
+
pattern: await searchParams
|
|
84
|
+
- not:
|
|
85
|
+
has:
|
|
86
|
+
pattern: Promise.all($$)
|
|
87
|
+
- not:
|
|
88
|
+
has:
|
|
89
|
+
pattern: Promise.allSettled($$)
|
|
90
|
+
- all:
|
|
91
|
+
- pattern: const $X = await $A
|
|
92
|
+
- not:
|
|
93
|
+
pattern: const $X = await params
|
|
94
|
+
- not:
|
|
95
|
+
pattern: const $X = await searchParams
|
|
96
|
+
- not:
|
|
97
|
+
pattern: const $X = await Promise.all($$$)
|
|
98
|
+
- not:
|
|
99
|
+
pattern: const $X = await Promise.allSettled($$$)
|
|
100
|
+
- all:
|
|
101
|
+
- pattern: let $X = await $A
|
|
102
|
+
- not:
|
|
103
|
+
pattern: let $X = await params
|
|
104
|
+
- not:
|
|
105
|
+
pattern: let $X = await searchParams
|
|
106
|
+
- not:
|
|
107
|
+
pattern: let $X = await Promise.all($$$)
|
|
108
|
+
- not:
|
|
109
|
+
pattern: let $X = await Promise.allSettled($$$)
|
|
110
|
+
- all:
|
|
111
|
+
- pattern: var $X = await $A
|
|
112
|
+
- not:
|
|
113
|
+
pattern: var $X = await params
|
|
114
|
+
- not:
|
|
115
|
+
pattern: var $X = await searchParams
|
|
116
|
+
- not:
|
|
117
|
+
pattern: var $X = await Promise.all($$$)
|
|
118
|
+
- not:
|
|
119
|
+
pattern: var $X = await Promise.allSettled($$$)
|
|
120
|
+
- all:
|
|
121
|
+
- pattern:
|
|
122
|
+
context: $X = await $A
|
|
123
|
+
selector: expression_statement
|
|
124
|
+
- not:
|
|
125
|
+
pattern: $X = await params
|
|
126
|
+
- not:
|
|
127
|
+
pattern: $X = await searchParams
|
|
128
|
+
- not:
|
|
129
|
+
pattern: $X = await Promise.all($$$)
|
|
130
|
+
- not:
|
|
131
|
+
pattern: $X = await Promise.allSettled($$$)
|
|
132
|
+
- precedes:
|
|
133
|
+
stopBy: neighbor
|
|
134
|
+
all:
|
|
135
|
+
- any:
|
|
136
|
+
- all:
|
|
137
|
+
- pattern:
|
|
138
|
+
context: await $B
|
|
139
|
+
selector: expression_statement
|
|
140
|
+
- not:
|
|
141
|
+
pattern: await params
|
|
142
|
+
- not:
|
|
143
|
+
pattern: await searchParams
|
|
144
|
+
- not:
|
|
145
|
+
has:
|
|
146
|
+
pattern: Promise.all($$)
|
|
147
|
+
- not:
|
|
148
|
+
has:
|
|
149
|
+
pattern: Promise.allSettled($$)
|
|
150
|
+
- all:
|
|
151
|
+
- pattern: const $Y = await $B
|
|
152
|
+
- not:
|
|
153
|
+
pattern: const $Y = await params
|
|
154
|
+
- not:
|
|
155
|
+
pattern: const $Y = await searchParams
|
|
156
|
+
- not:
|
|
157
|
+
pattern: const $Y = await Promise.all($$$)
|
|
158
|
+
- not:
|
|
159
|
+
pattern: const $Y = await Promise.allSettled($$$)
|
|
160
|
+
- all:
|
|
161
|
+
- pattern: let $Y = await $B
|
|
162
|
+
- not:
|
|
163
|
+
pattern: let $Y = await params
|
|
164
|
+
- not:
|
|
165
|
+
pattern: let $Y = await searchParams
|
|
166
|
+
- not:
|
|
167
|
+
pattern: let $Y = await Promise.all($$$)
|
|
168
|
+
- not:
|
|
169
|
+
pattern: let $Y = await Promise.allSettled($$$)
|
|
170
|
+
- all:
|
|
171
|
+
- pattern: var $Y = await $B
|
|
172
|
+
- not:
|
|
173
|
+
pattern: var $Y = await params
|
|
174
|
+
- not:
|
|
175
|
+
pattern: var $Y = await searchParams
|
|
176
|
+
- not:
|
|
177
|
+
pattern: var $Y = await Promise.all($$$)
|
|
178
|
+
- not:
|
|
179
|
+
pattern: var $Y = await Promise.allSettled($$$)
|
|
180
|
+
- all:
|
|
181
|
+
- pattern:
|
|
182
|
+
context: $Y = await $B
|
|
183
|
+
selector: expression_statement
|
|
184
|
+
- not:
|
|
185
|
+
pattern: $Y = await params
|
|
186
|
+
- not:
|
|
187
|
+
pattern: $Y = await searchParams
|
|
188
|
+
- not:
|
|
189
|
+
pattern: $Y = await Promise.all($$$)
|
|
190
|
+
- not:
|
|
191
|
+
pattern: $Y = await Promise.allSettled($$$)
|
|
192
|
+
- precedes:
|
|
193
|
+
stopBy: neighbor
|
|
194
|
+
any:
|
|
195
|
+
- all:
|
|
196
|
+
- pattern:
|
|
197
|
+
context: await $C
|
|
198
|
+
selector: expression_statement
|
|
199
|
+
- not:
|
|
200
|
+
pattern: await params
|
|
201
|
+
- not:
|
|
202
|
+
pattern: await searchParams
|
|
203
|
+
- not:
|
|
204
|
+
has:
|
|
205
|
+
pattern: Promise.all($$)
|
|
206
|
+
- not:
|
|
207
|
+
has:
|
|
208
|
+
pattern: Promise.allSettled($$)
|
|
209
|
+
- all:
|
|
210
|
+
- pattern: const $Z = await $C
|
|
211
|
+
- not:
|
|
212
|
+
pattern: const $Z = await params
|
|
213
|
+
- not:
|
|
214
|
+
pattern: const $Z = await searchParams
|
|
215
|
+
- not:
|
|
216
|
+
pattern: const $Z = await Promise.all($$$)
|
|
217
|
+
- not:
|
|
218
|
+
pattern: const $Z = await Promise.allSettled($$$)
|
|
219
|
+
- all:
|
|
220
|
+
- pattern: let $Z = await $C
|
|
221
|
+
- not:
|
|
222
|
+
pattern: let $Z = await params
|
|
223
|
+
- not:
|
|
224
|
+
pattern: let $Z = await searchParams
|
|
225
|
+
- not:
|
|
226
|
+
pattern: let $Z = await Promise.all($$$)
|
|
227
|
+
- not:
|
|
228
|
+
pattern: let $Z = await Promise.allSettled($$$)
|
|
229
|
+
- all:
|
|
230
|
+
- pattern: var $Z = await $C
|
|
231
|
+
- not:
|
|
232
|
+
pattern: var $Z = await params
|
|
233
|
+
- not:
|
|
234
|
+
pattern: var $Z = await searchParams
|
|
235
|
+
- not:
|
|
236
|
+
pattern: var $Z = await Promise.all($$$)
|
|
237
|
+
- not:
|
|
238
|
+
pattern: var $Z = await Promise.allSettled($$$)
|
|
239
|
+
- all:
|
|
240
|
+
- pattern:
|
|
241
|
+
context: $Z = await $C
|
|
242
|
+
selector: expression_statement
|
|
243
|
+
- not:
|
|
244
|
+
pattern: $Z = await params
|
|
245
|
+
- not:
|
|
246
|
+
pattern: $Z = await searchParams
|
|
247
|
+
- not:
|
|
248
|
+
pattern: $Z = await Promise.all($$$)
|
|
249
|
+
- not:
|
|
250
|
+
pattern: $Z = await Promise.allSettled($$$)
|
|
251
|
+
constraints:
|
|
252
|
+
A:
|
|
253
|
+
not:
|
|
254
|
+
regex: '^Promise\.all(Settled)?\b'
|
|
255
|
+
B:
|
|
256
|
+
not:
|
|
257
|
+
regex: '^Promise\.all(Settled)?\b'
|
|
258
|
+
C:
|
|
259
|
+
not:
|
|
260
|
+
regex: '^Promise\.all(Settled)?\b'
|
|
261
|
+
P:
|
|
262
|
+
not:
|
|
263
|
+
regex: '^Promise\.all(Settled)?\b'
|
|
264
|
+
examples:
|
|
265
|
+
- code: |
|
|
266
|
+
async function run() {
|
|
267
|
+
await one()
|
|
268
|
+
await two()
|
|
269
|
+
await three()
|
|
270
|
+
}
|
|
271
|
+
isValid: false
|
|
272
|
+
file: 'web/app/example.tsx'
|
|
273
|
+
- code: |
|
|
274
|
+
async function run() {
|
|
275
|
+
const { id } = await params
|
|
276
|
+
const topic = await getTopic(id)
|
|
277
|
+
const aliases = await getAliases(id)
|
|
278
|
+
return <div>{topic.name}{aliases.length}</div>
|
|
279
|
+
}
|
|
280
|
+
isValid: true
|
|
281
|
+
file: 'web/app/example.tsx'
|
|
282
|
+
- code: |
|
|
283
|
+
async function run() {
|
|
284
|
+
await Promise.all([one(), two(), three()])
|
|
285
|
+
return <div />
|
|
286
|
+
}
|
|
287
|
+
isValid: true
|
|
288
|
+
file: 'web/app/example.tsx'
|