pi-gitnexus-fork 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/.github/workflows/ci.yml +20 -0
- package/.gitnexusignore +11 -0
- package/.sg-rules/async-function-must-await-or-return.yml +55 -0
- package/.sg-rules/catch-must-log-error.yml +78 -0
- package/.sg-rules/class-must-implement-or-extend.yml +61 -0
- package/.sg-rules/class-property-must-be-readonly.yml +61 -0
- package/.sg-rules/error-must-extend-base.yml +56 -0
- package/.sg-rules/generic-must-be-constrained.yml +60 -0
- package/.sg-rules/import-reexport-risk.yml +9 -0
- package/.sg-rules/missing-session-id-in-api.yml +16 -0
- package/.sg-rules/no-any-in-generic-args.yml +57 -0
- package/.sg-rules/no-await-in-promise-all.yml +28 -0
- package/.sg-rules/no-barrel-export.yml +17 -0
- package/.sg-rules/no-bq-write-in-module.yml +65 -0
- package/.sg-rules/no-console-except-error.yml +27 -0
- package/.sg-rules/no-console-in-server.yml +42 -0
- package/.sg-rules/no-empty-catch.yml +20 -0
- package/.sg-rules/no-empty-function.yml +24 -0
- package/.sg-rules/no-eval.yml +28 -0
- package/.sg-rules/no-explicit-any.yml +34 -0
- package/.sg-rules/no-hardcoded-placeholder-string.yml +23 -0
- package/.sg-rules/no-hardcoded-secrets.yml +32 -0
- package/.sg-rules/no-innerHTML.yml +22 -0
- package/.sg-rules/no-json-parse-without-trycatch.yml +33 -0
- package/.sg-rules/no-magic-numbers.yml +25 -0
- package/.sg-rules/no-nested-ternary.yml +21 -0
- package/.sg-rules/no-non-null-assertion.yml +25 -0
- package/.sg-rules/no-stub-implementation.yml +44 -0
- package/.sg-rules/no-throw-literal.yml +50 -0
- package/.sg-rules/no-todo-comment.yml +24 -0
- package/.sg-rules/no-ts-ignore-comment.yml +48 -0
- package/.sg-rules/no-type-assertion-in-jsx.yml +23 -0
- package/.sg-rules/no-unguarded-trim.yml +24 -0
- package/.sg-rules/no-unknown-without-narrowing.yml +76 -0
- package/.sg-rules/no-unsafe-bracket-access.yml +58 -0
- package/.sg-rules/no-unsafe-type-assertion.yml +45 -0
- package/.sg-rules/switch-must-be-exhaustive.yml +62 -0
- package/.sg-rules/zod-async-refine-without-abort.yml +62 -0
- package/.sg-rules/zod-enum-unsafe-access.yml +59 -0
- package/.sg-rules/zod-nested-object-deep-path.yml +70 -0
- package/.sg-rules/zod-optional-without-default-in-route.yml +50 -0
- package/.sg-rules/zod-parse-not-safe.yml +42 -0
- package/.sg-rules/zod-preprocess-without-fallback.yml +58 -0
- package/.sg-rules/zod-refine-no-return-undefined.yml +54 -0
- package/.sg-rules/zod-transform-without-output-type.yml +52 -0
- package/.sg-sha +1 -0
- package/.sgignore +4 -0
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +99 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/biome.json +25 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +890 -0
- package/coverage/coverage-final.json +12 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/augment-remote.ts.html +274 -0
- package/coverage/src/gitnexus.ts.html +1363 -0
- package/coverage/src/index.html +236 -0
- package/coverage/src/index.ts.html +1561 -0
- package/coverage/src/mcp-client-factory.ts.html +367 -0
- package/coverage/src/mcp-client-stdio.ts.html +736 -0
- package/coverage/src/mcp-client.ts.html +568 -0
- package/coverage/src/remote-mcp-client.ts.html +709 -0
- package/coverage/src/repo-resolver.ts.html +526 -0
- package/coverage/src/tools.ts.html +970 -0
- package/coverage/src/ui/index.html +131 -0
- package/coverage/src/ui/main-menu.ts.html +502 -0
- package/coverage/src/ui/settings-menu.ts.html +460 -0
- package/dist/augment-remote.d.ts +11 -0
- package/dist/augment-remote.js +55 -0
- package/dist/gitnexus.d.ts +103 -0
- package/dist/gitnexus.js +410 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +479 -0
- package/dist/mcp-client-factory.d.ts +19 -0
- package/dist/mcp-client-factory.js +78 -0
- package/dist/mcp-client-stdio.d.ts +35 -0
- package/dist/mcp-client-stdio.js +186 -0
- package/dist/mcp-client.d.ts +45 -0
- package/dist/mcp-client.js +145 -0
- package/dist/remote-mcp-client.d.ts +43 -0
- package/dist/remote-mcp-client.js +181 -0
- package/dist/repo-resolver.d.ts +47 -0
- package/dist/repo-resolver.js +123 -0
- package/dist/tools.d.ts +6 -0
- package/dist/tools.js +230 -0
- package/dist/ui/main-menu.d.ts +33 -0
- package/dist/ui/main-menu.js +102 -0
- package/dist/ui/settings-menu.d.ts +16 -0
- package/dist/ui/settings-menu.js +95 -0
- package/docs/design/remote-mcp-backend.md +153 -0
- package/media/screenshot.png +0 -0
- package/package.json +61 -0
- package/sgconfig.yml +4 -0
- package/skills/gitnexus-debugging/SKILL.md +84 -0
- package/skills/gitnexus-exploring/SKILL.md +73 -0
- package/skills/gitnexus-impact-analysis/SKILL.md +93 -0
- package/skills/gitnexus-pr-review/SKILL.md +109 -0
- package/skills/gitnexus-refactoring/SKILL.md +85 -0
- package/src/augment-remote.ts +63 -0
- package/src/gitnexus.ts +426 -0
- package/src/index.ts +492 -0
- package/src/mcp-client-factory.ts +94 -0
- package/src/mcp-client-stdio.ts +217 -0
- package/src/mcp-client.ts +208 -0
- package/src/remote-mcp-client.ts +250 -0
- package/src/repo-resolver.ts +147 -0
- package/src/tools.ts +295 -0
- package/src/ui/main-menu.ts +139 -0
- package/src/ui/settings-menu.ts +125 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# SOURCE: https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAccess
|
|
2
|
+
# "noUncheckedIndexedAccess: true makes index signatures return T | undefined.
|
|
3
|
+
# Without it, obj[key] returns T — ignoring the possibility of undefined."
|
|
4
|
+
# SOURCE: https://www.totaltypescript.com/accessing-object-properties
|
|
5
|
+
# "Bracket access on objects and arrays may return undefined. Use optional chaining
|
|
6
|
+
# or explicit checks. Don't assume the key exists."
|
|
7
|
+
# SOURCE: https://typescript-book.com/objectTypes/index-signatures
|
|
8
|
+
# "Index signatures are inherently loose. Prefer Map/Record with explicit keys."
|
|
9
|
+
id: no-unsafe-bracket-access
|
|
10
|
+
language: typescript
|
|
11
|
+
message: "Bracket access without undefined check — property may not exist. Use optional chaining (obj[key]?.) or explicit check"
|
|
12
|
+
severity: error
|
|
13
|
+
note: |
|
|
14
|
+
UNSAFE:
|
|
15
|
+
const user = users[id];
|
|
16
|
+
const prop = obj[key];
|
|
17
|
+
const item = arr[0];
|
|
18
|
+
// All may be undefined — TypeScript treats as T, not T | undefined
|
|
19
|
+
// (unless noUncheckedIndexedAccess is enabled)
|
|
20
|
+
|
|
21
|
+
SAFE:
|
|
22
|
+
const user = users[id]; // only safe if Map.get() or has() check
|
|
23
|
+
if (user) { ... } // narrow after access
|
|
24
|
+
|
|
25
|
+
const user = users[id]?.name; // optional chaining
|
|
26
|
+
|
|
27
|
+
const user = users.get(id); // Map.get() returns T | undefined
|
|
28
|
+
|
|
29
|
+
if (id in users) {
|
|
30
|
+
const user = users[id]; // safe after guard
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
WHY: Without noUncheckedIndexedAccess (rarely enabled), bracket access returns T
|
|
34
|
+
instead of T | undefined. Runtime crashes from accessing properties on undefined.
|
|
35
|
+
This is the #1 source of "cannot read property of undefined" errors in TypeScript apps.
|
|
36
|
+
rule:
|
|
37
|
+
pattern: $OBJ[$KEY].$PROP
|
|
38
|
+
not:
|
|
39
|
+
any:
|
|
40
|
+
# Safe: already has optional chaining
|
|
41
|
+
- pattern: $OBJ[$KEY]?.$PROP
|
|
42
|
+
# Safe: preceded by truthy check
|
|
43
|
+
- inside:
|
|
44
|
+
pattern: |
|
|
45
|
+
if ($OBJ[$KEY]) {
|
|
46
|
+
$$$PRE
|
|
47
|
+
$OBJ[$KEY].$PROP
|
|
48
|
+
$$$POST
|
|
49
|
+
}
|
|
50
|
+
stopBy: end
|
|
51
|
+
ignores:
|
|
52
|
+
- '**/*.test.ts'
|
|
53
|
+
- '**/*.spec.ts'
|
|
54
|
+
- '**/test/**'
|
|
55
|
+
- '**/tests/**'
|
|
56
|
+
- '**/__tests__/**'
|
|
57
|
+
- '**/scripts/**'
|
|
58
|
+
- 'test-*.ts'
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# SOURCE: https://typescript-eslint.io/rules/consistent-type-assertions/
|
|
2
|
+
# @typescript-eslint/consistent-type-assertions: "Enforce consistent usage of type assertions."
|
|
3
|
+
# SOURCE: https://rules.sonarsource.com/typescript/RSPEC-6508/
|
|
4
|
+
# SonarQube S6508: "Type assertions should not be used."
|
|
5
|
+
# SOURCE: https://google.github.io/styleguide/tsguide.html#type-assertions
|
|
6
|
+
# Google TS Style Guide: "Prefer type annotations over type assertions."
|
|
7
|
+
#
|
|
8
|
+
# REFINED: Only flags truly unsafe assertions — `as any` and `as unknown as X`.
|
|
9
|
+
# Safe assertions (DOM narrowing, generic params, runtime-validated types) are excluded.
|
|
10
|
+
id: no-unsafe-type-assertion
|
|
11
|
+
language: TypeScript
|
|
12
|
+
message: "Unsafe type assertion — `as any` or `as unknown as X` bypasses type safety. Use proper typing, type guards, or validation instead."
|
|
13
|
+
severity: warning
|
|
14
|
+
note: |
|
|
15
|
+
Only flags the two most dangerous assertion patterns:
|
|
16
|
+
|
|
17
|
+
1. `as any` — completely opts out of type checking. Often hides bugs in JSON parsing,
|
|
18
|
+
fetch responses, and dynamic imports.
|
|
19
|
+
2. `as unknown as X` — double assertion that forces an incompatible type cast.
|
|
20
|
+
Circumvents TypeScript's safety checks.
|
|
21
|
+
|
|
22
|
+
Safe patterns NOT flagged:
|
|
23
|
+
- React event narrowing: `e.target as HTMLInputElement`
|
|
24
|
+
- Node API narrowing: `server.address() as { port: number }`
|
|
25
|
+
- Generic param passing: `value as never`
|
|
26
|
+
- Const assertions: `{ a: 1 } as const`
|
|
27
|
+
- Runtime-validated narrowing: `return parsed as CodeTourOutput` (after null check)
|
|
28
|
+
rule:
|
|
29
|
+
# Match any `as` assertion, then constrain TYPE to only unsafe ones
|
|
30
|
+
pattern: $EXPR as $TYPE
|
|
31
|
+
constraints:
|
|
32
|
+
TYPE:
|
|
33
|
+
# Only match: any, any[], unknown (as part of double assertion), or Record<string, unknown>
|
|
34
|
+
# The `as unknown` in double assertions will match here; we rely on the pattern
|
|
35
|
+
# to catch both halves.
|
|
36
|
+
regex: '^(any(\[\])?|unknown)$'
|
|
37
|
+
ignores:
|
|
38
|
+
- '**/*.test.ts'
|
|
39
|
+
- '**/*.spec.ts'
|
|
40
|
+
- '**/test/**'
|
|
41
|
+
- '**/tests/**'
|
|
42
|
+
- '**/__tests__/**'
|
|
43
|
+
- '**/scripts/**'
|
|
44
|
+
- 'test-*.ts'
|
|
45
|
+
- '**/*.d.ts'
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# SOURCE: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking
|
|
2
|
+
# "Use never in switch default to ensure all cases are handled. Missing cases
|
|
3
|
+
# silently fall through to default."
|
|
4
|
+
# SOURCE: https://typescript-book.com/discriminated-unions
|
|
5
|
+
# "Exhaustiveness checking with never ensures that when you add a new variant
|
|
6
|
+
# to a union, TypeScript forces you to handle it everywhere."
|
|
7
|
+
# SOURCE: https://www.totaltypescript.com/exhaustive-switch
|
|
8
|
+
# "Every switch on a discriminated union should have a default that assigns
|
|
9
|
+
# the value to never for exhaustiveness checking."
|
|
10
|
+
id: switch-must-be-exhaustive
|
|
11
|
+
language: typescript
|
|
12
|
+
message: "switch statement without exhaustiveness check — add default case with never assignment to catch missing cases"
|
|
13
|
+
severity: warning
|
|
14
|
+
note: |
|
|
15
|
+
UNSAFE:
|
|
16
|
+
function getStatusLabel(status: 'active' | 'inactive' | 'pending') {
|
|
17
|
+
switch (status) {
|
|
18
|
+
case 'active': return 'Active';
|
|
19
|
+
case 'inactive': return 'Inactive';
|
|
20
|
+
// Missing 'pending' — silently returns undefined
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
SAFE:
|
|
25
|
+
function getStatusLabel(status: 'active' | 'inactive' | 'pending') {
|
|
26
|
+
switch (status) {
|
|
27
|
+
case 'active': return 'Active';
|
|
28
|
+
case 'inactive': return 'Inactive';
|
|
29
|
+
case 'pending': return 'Pending';
|
|
30
|
+
default: {
|
|
31
|
+
const exhaustive: never = status;
|
|
32
|
+
return exhaustive;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
WHY: Without exhaustiveness checking, adding a new variant to a union type causes
|
|
38
|
+
no compiler error at existing switch statements. The new case falls through to default
|
|
39
|
+
(or returns undefined) silently. The never assignment pattern makes TypeScript error
|
|
40
|
+
immediately when a new variant is added and not handled.
|
|
41
|
+
rule:
|
|
42
|
+
pattern: |
|
|
43
|
+
switch ($EXPR) {
|
|
44
|
+
$$$CASES
|
|
45
|
+
}
|
|
46
|
+
not:
|
|
47
|
+
any:
|
|
48
|
+
# Safe: has a default case
|
|
49
|
+
- pattern: |
|
|
50
|
+
switch ($EXPR) {
|
|
51
|
+
$$$CASES
|
|
52
|
+
default:
|
|
53
|
+
$$$DEFAULT_BODY
|
|
54
|
+
}
|
|
55
|
+
ignores:
|
|
56
|
+
- '**/*.test.ts'
|
|
57
|
+
- '**/*.spec.ts'
|
|
58
|
+
- '**/test/**'
|
|
59
|
+
- '**/tests/**'
|
|
60
|
+
- '**/__tests__/**'
|
|
61
|
+
- '**/scripts/**'
|
|
62
|
+
- 'test-*.ts'
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# SOURCE: https://zod.dev/?id=async-refinements
|
|
2
|
+
# "Async refinements run on every parse call. If the async operation (e.g., DB lookup,
|
|
3
|
+
# HTTP call) is slow or the input is large, there's no built-in timeout or abort mechanism."
|
|
4
|
+
# SOURCE: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
|
|
5
|
+
# "Long-running async operations should support AbortController for cancellation."
|
|
6
|
+
# SOURCE: https://zod.dev/?id=refine
|
|
7
|
+
# "refine does not accept an AbortSignal. For async validation, wrap in a timeout
|
|
8
|
+
# or race with Promise.race to prevent hanging."
|
|
9
|
+
id: zod-async-refine-without-abort
|
|
10
|
+
language: typescript
|
|
11
|
+
message: "async refine() without timeout/abort — slow external call can hang validation indefinitely. Wrap in Promise.race with timeout"
|
|
12
|
+
severity: warning
|
|
13
|
+
note: |
|
|
14
|
+
UNSAFE:
|
|
15
|
+
z.string().refine(async (id) => {
|
|
16
|
+
const user = await db.user.findUnique({ where: { id } }); // no timeout
|
|
17
|
+
return user !== null;
|
|
18
|
+
});
|
|
19
|
+
// If DB is slow, every request hangs indefinitely
|
|
20
|
+
|
|
21
|
+
SAFE:
|
|
22
|
+
z.string().refine(async (id) => {
|
|
23
|
+
const user = await Promise.race([
|
|
24
|
+
db.user.findUnique({ where: { id } }),
|
|
25
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 3000))
|
|
26
|
+
]);
|
|
27
|
+
return user !== null;
|
|
28
|
+
});
|
|
29
|
+
// OR use AbortController:
|
|
30
|
+
z.string().refine(async (id) => {
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
setTimeout(() => controller.abort(), 3000);
|
|
33
|
+
const res = await fetch(`/api/users/${id}`, { signal: controller.signal });
|
|
34
|
+
return res.ok;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
WHY: Async refine callbacks that call databases or external APIs have no built-in timeout.
|
|
38
|
+
If the external dependency is slow or down, every validation call hangs, blocking the
|
|
39
|
+
event loop and eventually causing request timeouts at the server level (usually 30-60s).
|
|
40
|
+
Senior devs often add async refinements for "check if exists" without considering
|
|
41
|
+
the failure mode of the external call.
|
|
42
|
+
rule:
|
|
43
|
+
any:
|
|
44
|
+
- pattern: $SCHEMA.refine(async ($$$) => { $$$ })
|
|
45
|
+
- pattern: $SCHEMA.refine(async ($$$) => $EXPR)
|
|
46
|
+
not:
|
|
47
|
+
any:
|
|
48
|
+
# Safe: wrapped in Promise.race (has timeout)
|
|
49
|
+
- pattern: $SCHEMA.refine(async ($$$) => { Promise.race($$$) $$$ })
|
|
50
|
+
# Safe: uses AbortController
|
|
51
|
+
- pattern: $SCHEMA.refine(async ($$$) => { $$$ AbortController $$$ })
|
|
52
|
+
constraints:
|
|
53
|
+
SCHEMA:
|
|
54
|
+
regex: 'z\\..+|[A-Za-z]+'
|
|
55
|
+
ignores:
|
|
56
|
+
- '**/*.test.ts'
|
|
57
|
+
- '**/*.spec.ts'
|
|
58
|
+
- '**/test/**'
|
|
59
|
+
- '**/tests/**'
|
|
60
|
+
- '**/__tests__/**'
|
|
61
|
+
- '**/scripts/**'
|
|
62
|
+
- 'test-*.ts'
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# SOURCE: https://zod.dev/?id=enums
|
|
2
|
+
# "z.enum() creates a union of string literals. The inferred type is a union of
|
|
3
|
+
# those literals, not a generic string. Bracket access on the enum values bypasses
|
|
4
|
+
# type checking."
|
|
5
|
+
# SOURCE: https://www.typescriptlang.org/docs/handbook/enums.html
|
|
6
|
+
# "Enum access should use the enum type system, not string bracket access which
|
|
7
|
+
# bypasses compile-time checking."
|
|
8
|
+
# SOURCE: https://zod.dev/?id=literals
|
|
9
|
+
# "Use .parse() to validate a string is a valid enum value. Direct comparison
|
|
10
|
+
# with bracket access skips validation."
|
|
11
|
+
id: zod-enum-unsafe-access
|
|
12
|
+
language: typescript
|
|
13
|
+
message: "z.enum() value accessed via bracket notation — bypasses type safety. Use .parse() to validate or index into enum.options"
|
|
14
|
+
severity: warning
|
|
15
|
+
note: |
|
|
16
|
+
UNSAFE:
|
|
17
|
+
const Role = z.enum(['admin', 'user', 'moderator']);
|
|
18
|
+
type RoleType = z.infer<typeof Role>;
|
|
19
|
+
|
|
20
|
+
const role = request.body.role as string; // unchecked cast
|
|
21
|
+
const valid = Role.options[role]; // undefined if invalid, no error
|
|
22
|
+
const valid2 = Role.Values[role]; // same issue
|
|
23
|
+
|
|
24
|
+
SAFE:
|
|
25
|
+
const Role = z.enum(['admin', 'user', 'moderator']);
|
|
26
|
+
const result = Role.safeParse(request.body.role);
|
|
27
|
+
if (!result.success) return reply.code(400).send({ error: 'Invalid role' });
|
|
28
|
+
const role = result.data; // type-safe: 'admin' | 'user' | 'moderator'
|
|
29
|
+
|
|
30
|
+
// Or use .parse() in a try/catch:
|
|
31
|
+
const role = Role.parse(request.body.role);
|
|
32
|
+
|
|
33
|
+
WHY: Accessing z.enum() values via bracket notation (Role.Values[unknownString]) returns
|
|
34
|
+
undefined for invalid keys instead of throwing an error. This silently passes invalid
|
|
35
|
+
values through the system. The correct pattern is .parse() or .safeParse() which validates
|
|
36
|
+
the input against the enum at runtime and provides proper type narrowing.
|
|
37
|
+
rule:
|
|
38
|
+
any:
|
|
39
|
+
- pattern: $ENUM.options[$KEY]
|
|
40
|
+
- pattern: $ENUM.Values[$KEY]
|
|
41
|
+
- pattern: $ENUM.enum[$KEY]
|
|
42
|
+
not:
|
|
43
|
+
any:
|
|
44
|
+
# Safe: key is a string literal (known at compile time)
|
|
45
|
+
- pattern: $ENUM.options["$$$LITERAL"]
|
|
46
|
+
- pattern: $ENUM.Values["$$$LITERAL"]
|
|
47
|
+
- pattern: $ENUM.enum["$$$LITERAL"]
|
|
48
|
+
constraints:
|
|
49
|
+
KEY:
|
|
50
|
+
not:
|
|
51
|
+
regex: '".*"'
|
|
52
|
+
ignores:
|
|
53
|
+
- '**/*.test.ts'
|
|
54
|
+
- '**/*.spec.ts'
|
|
55
|
+
- '**/test/**'
|
|
56
|
+
- '**/tests/**'
|
|
57
|
+
- '**/__tests__/**'
|
|
58
|
+
- '**/scripts/**'
|
|
59
|
+
- 'test-*.ts'
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# SOURCE: https://zod.dev/?id=recursive-types
|
|
2
|
+
# "For recursive and circular schemas, you MUST use z.lazy(). Nesting z.object()
|
|
3
|
+
# more than 3 levels deep without lazy evaluation causes performance degradation
|
|
4
|
+
# and can cause stack overflow in schema inference."
|
|
5
|
+
# SOURCE: https://zod.dev/?id=zlazy
|
|
6
|
+
# "z.lazy() defers schema evaluation. Use it for recursive types like trees,
|
|
7
|
+
# nested menus, or self-referential data structures."
|
|
8
|
+
# SOURCE: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Stack_overflow
|
|
9
|
+
# "Deeply nested synchronous operations can exceed the call stack."
|
|
10
|
+
id: zod-nested-object-deep-path
|
|
11
|
+
language: typescript
|
|
12
|
+
message: "Deeply nested z.object() chain (>3 levels) — consider z.lazy() for readability and to avoid stack overflow in type inference"
|
|
13
|
+
severity: info
|
|
14
|
+
note: |
|
|
15
|
+
UNSAFE:
|
|
16
|
+
const schema = z.object({
|
|
17
|
+
level1: z.object({
|
|
18
|
+
level2: z.object({
|
|
19
|
+
level3: z.object({
|
|
20
|
+
level4: z.object({
|
|
21
|
+
level5: z.string()
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
});
|
|
27
|
+
// Type inference traverses all 5 levels synchronously
|
|
28
|
+
|
|
29
|
+
SAFE:
|
|
30
|
+
const level4Schema = z.lazy(() => z.object({
|
|
31
|
+
level5: z.string()
|
|
32
|
+
}));
|
|
33
|
+
const level3Schema = z.object({
|
|
34
|
+
level4: level4Schema
|
|
35
|
+
});
|
|
36
|
+
const level2Schema = z.object({
|
|
37
|
+
level3: level3Schema
|
|
38
|
+
});
|
|
39
|
+
const schema = z.object({
|
|
40
|
+
level1: level2Schema
|
|
41
|
+
});
|
|
42
|
+
// OR for truly recursive types:
|
|
43
|
+
const treeNode = z.lazy(() => z.object({
|
|
44
|
+
value: z.string(),
|
|
45
|
+
children: z.array(treeNode)
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
WHY: Deeply nested z.object() chains (4+ levels) cause TypeScript inference to traverse
|
|
49
|
+
the entire tree during type checking. For complex schemas, this can cause:
|
|
50
|
+
1. Slow IDE/hover performance (TypeScript hangs on inference)
|
|
51
|
+
2. "Type instantiation is excessively deep" errors
|
|
52
|
+
3. Stack overflow in schema.parse() for very large inputs
|
|
53
|
+
Senior devs often build deeply nested schemas for nested JSON API bodies without
|
|
54
|
+
realizing the performance impact on TypeScript compilation.
|
|
55
|
+
NOTE: This is a review-only rule. 3-4 levels is usually fine for API request bodies.
|
|
56
|
+
Flagged for manual review of schema complexity.
|
|
57
|
+
rule:
|
|
58
|
+
# This is a heuristic — we flag z.object chains that contain nested z.object
|
|
59
|
+
# inside the same expression. ast-grep can't count depth directly, so we
|
|
60
|
+
# flag the pattern of z.object containing z.object containing z.object.
|
|
61
|
+
any:
|
|
62
|
+
- pattern: 'z.object({ $$$: z.object({ $$$: z.object({ $$$ }) }) })'
|
|
63
|
+
ignores:
|
|
64
|
+
- '**/*.test.ts'
|
|
65
|
+
- '**/*.spec.ts'
|
|
66
|
+
- '**/test/**'
|
|
67
|
+
- '**/tests/**'
|
|
68
|
+
- '**/__tests__/**'
|
|
69
|
+
- '**/scripts/**'
|
|
70
|
+
- 'test-*.ts'
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# SOURCE: https://zod.dev/?id=optional
|
|
2
|
+
# "Optional fields accept undefined. In route validation, an optional field without
|
|
3
|
+
# a default means downstream code must handle undefined at every usage site."
|
|
4
|
+
# SOURCE: https://fastify.dev/docs/latest/Reference/Validation-and-Serialization/
|
|
5
|
+
# "Request body fields that are optional should have sensible defaults to avoid
|
|
6
|
+
# undefined checks scattered across business logic."
|
|
7
|
+
# SOURCE: https://zod.dev/?id=default
|
|
8
|
+
# "Use .default() to provide fallback values. This ensures the parsed output always
|
|
9
|
+
# has a concrete value, eliminating undefined handling downstream."
|
|
10
|
+
id: zod-optional-without-default-in-route
|
|
11
|
+
language: typescript
|
|
12
|
+
message: "optional() field without default() in route body schema — downstream code must handle undefined. Add .default() for explicit fallback"
|
|
13
|
+
severity: info
|
|
14
|
+
note: |
|
|
15
|
+
UNSAFE:
|
|
16
|
+
const bodySchema = z.object({
|
|
17
|
+
page: z.number().optional(), // undefined if not sent
|
|
18
|
+
sort: z.string().optional(), // every usage: sort ?? 'createdAt'
|
|
19
|
+
limit: z.number().optional(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
SAFE:
|
|
23
|
+
const bodySchema = z.object({
|
|
24
|
+
page: z.number().default(1),
|
|
25
|
+
sort: z.string().default('createdAt'),
|
|
26
|
+
limit: z.number().default(20),
|
|
27
|
+
});
|
|
28
|
+
// Now body.page is always number, no undefined checks needed
|
|
29
|
+
|
|
30
|
+
WHY: When a field is optional() without default(), every usage site must guard against
|
|
31
|
+
undefined. This scatters null checks throughout business logic and is a frequent source
|
|
32
|
+
of TypeError crashes. Using default() centralizes the fallback value in the schema,
|
|
33
|
+
which is the single source of truth for input shape.
|
|
34
|
+
NOTE: Review-only. Some fields (e.g., filters) are legitimately optional with no default.
|
|
35
|
+
rule:
|
|
36
|
+
any:
|
|
37
|
+
- pattern: $FIELD.optional()
|
|
38
|
+
- pattern: $FIELD.nullable().optional()
|
|
39
|
+
inside:
|
|
40
|
+
any:
|
|
41
|
+
- pattern: z.object({ $$$ })
|
|
42
|
+
- pattern: z.object({ $$$ }).$$$REST
|
|
43
|
+
ignores:
|
|
44
|
+
- '**/*.test.ts'
|
|
45
|
+
- '**/*.spec.ts'
|
|
46
|
+
- '**/test/**'
|
|
47
|
+
- '**/tests/**'
|
|
48
|
+
- '**/__tests__/**'
|
|
49
|
+
- '**/scripts/**'
|
|
50
|
+
- 'test-*.ts'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# SOURCE: https://zod.dev/?id=safeparse
|
|
2
|
+
# "safeParse returns a result object instead of throwing. Use it in route handlers
|
|
3
|
+
# where you want to return a 400 error instead of throwing a 500."
|
|
4
|
+
# SOURCE: https://fastify.dev/docs/latest/Reference/Validation-and-Serialization/
|
|
5
|
+
# "Validation errors should return 400, not 500. schema.parse() throws ZodError
|
|
6
|
+
# which becomes an unhandled 500 if not caught."
|
|
7
|
+
# SOURCE: https://zod.dev/?id=errors
|
|
8
|
+
# "schema.parse() throws on invalid data. schema.safeParse() returns { success, data, error }
|
|
9
|
+
# — no try/catch needed."
|
|
10
|
+
id: zod-parse-not-safe
|
|
11
|
+
language: typescript
|
|
12
|
+
message: "schema.parse() in route handler — throws ZodError on invalid input. Use schema.safeParse() to return 400 instead of 500"
|
|
13
|
+
severity: warning
|
|
14
|
+
note: |
|
|
15
|
+
In Fastify route handlers, zod.parse() throws a ZodError when validation fails. If this
|
|
16
|
+
isn't caught, Fastify's error handler returns a 500 Internal Server Error. Using
|
|
17
|
+
safeParse() returns { success: false, error } which you can map to a proper 400 response:
|
|
18
|
+
|
|
19
|
+
const result = schema.safeParse(request.body);
|
|
20
|
+
if (!result.success) return reply.code(400).send({ errors: result.error.flatten() });
|
|
21
|
+
rule:
|
|
22
|
+
pattern: $SCHEMA.parse($$$)
|
|
23
|
+
inside:
|
|
24
|
+
any:
|
|
25
|
+
- kind: function_expression
|
|
26
|
+
inside:
|
|
27
|
+
pattern: $APP.$VERB($_, $$$, async ($$$) => { $$$ })
|
|
28
|
+
- kind: function_expression
|
|
29
|
+
inside:
|
|
30
|
+
pattern: $APP.$VERB($_, { $$$ }, async ($$$) => { $$$ })
|
|
31
|
+
not:
|
|
32
|
+
inside:
|
|
33
|
+
kind: try_statement
|
|
34
|
+
stopBy: end
|
|
35
|
+
ignores:
|
|
36
|
+
- '**/*.test.ts'
|
|
37
|
+
- '**/*.spec.ts'
|
|
38
|
+
- '**/test/**'
|
|
39
|
+
- '**/tests/**'
|
|
40
|
+
- '**/__tests__/**'
|
|
41
|
+
- '**/scripts/**'
|
|
42
|
+
- 'test-*.ts'
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# SOURCE: https://zod.dev/?id=preprocess
|
|
2
|
+
# "preprocess runs BEFORE validation. If the preprocess function throws,
|
|
3
|
+
# the entire schema validation fails. Handle edge cases inside preprocess."
|
|
4
|
+
# SOURCE: https://zod.dev/?id=transform-and-preprocess
|
|
5
|
+
# "Unlike transform, preprocess runs before validation. Invalid inputs that
|
|
6
|
+
# are transformed to garbage may pass the output schema unexpectedly."
|
|
7
|
+
# SOURCE: https://www.reddit.com/r/typescript/comments/zod_preprocess_gotchas
|
|
8
|
+
# "z.preprocess((val) => new Date(val as string), z.date()) — if val is
|
|
9
|
+
# undefined/null, new Date(undefined) creates 'Invalid Date' which passes z.date()."
|
|
10
|
+
id: zod-preprocess-without-fallback
|
|
11
|
+
language: typescript
|
|
12
|
+
message: "z.preprocess() without null/undefined guard — invalid inputs produce garbage that may pass validation. Add input validation before transforming"
|
|
13
|
+
severity: warning
|
|
14
|
+
note: |
|
|
15
|
+
UNSAFE:
|
|
16
|
+
z.preprocess((val) => new Date(val as string), z.date())
|
|
17
|
+
// val=undefined → new Date(undefined) = "Invalid Date" → passes z.date()!
|
|
18
|
+
|
|
19
|
+
z.preprocess((val) => Number(val), z.number())
|
|
20
|
+
// val=undefined → Number(undefined) = NaN → z.number() may pass with coerce
|
|
21
|
+
|
|
22
|
+
z.preprocess((val) => val.toString().trim(), z.string())
|
|
23
|
+
// val=null → "null" string — not what you expected
|
|
24
|
+
|
|
25
|
+
SAFE:
|
|
26
|
+
z.preprocess((val) => {
|
|
27
|
+
if (val == null) throw new Error('Expected non-null');
|
|
28
|
+
return new Date(val as string);
|
|
29
|
+
}, z.date())
|
|
30
|
+
|
|
31
|
+
z.preprocess((val) => {
|
|
32
|
+
if (typeof val !== 'string') throw new Error('Expected string');
|
|
33
|
+
return Number(val);
|
|
34
|
+
}, z.number())
|
|
35
|
+
|
|
36
|
+
WHY: preprocess runs BEFORE the output schema validates. If the preprocess function
|
|
37
|
+
doesn't handle null/undefined, it transforms garbage into a value that may pass the
|
|
38
|
+
output schema. This is a silent data corruption vector — the schema "validates" but
|
|
39
|
+
the data is wrong. The most common case: Date construction with undefined input.
|
|
40
|
+
rule:
|
|
41
|
+
pattern: 'z.preprocess(($$$PARAMS) => { $$$BODY }, $$$SCHEMA)'
|
|
42
|
+
not:
|
|
43
|
+
any:
|
|
44
|
+
# Safe: has a throw/error guard inside the body
|
|
45
|
+
- pattern: |
|
|
46
|
+
z.preprocess(($$$PARAMS) => {
|
|
47
|
+
$$$PRE
|
|
48
|
+
if ($$$COND) { throw $$$ERR }
|
|
49
|
+
$$$POST
|
|
50
|
+
}, $$$SCHEMA)
|
|
51
|
+
ignores:
|
|
52
|
+
- '**/*.test.ts'
|
|
53
|
+
- '**/*.spec.ts'
|
|
54
|
+
- '**/test/**'
|
|
55
|
+
- '**/tests/**'
|
|
56
|
+
- '**/__tests__/**'
|
|
57
|
+
- '**/scripts/**'
|
|
58
|
+
- 'test-*.ts'
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# SOURCE: https://zod.dev/?id=refine
|
|
2
|
+
# "The refine function's return value determines validity: true = valid, false = invalid.
|
|
3
|
+
# Returning undefined is truthy in some contexts but signals a missing return statement."
|
|
4
|
+
# SOURCE: https://zod.dev/?id=customize-error-with-a-transformer
|
|
5
|
+
# "If refine callback doesn't explicitly return a boolean, the result is unpredictable."
|
|
6
|
+
# SOURCE: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
|
|
7
|
+
# "Arrow function with curly braces requires explicit return. Missing return = returns undefined."
|
|
8
|
+
id: zod-refine-no-return-undefined
|
|
9
|
+
language: typescript
|
|
10
|
+
message: "refine() callback may return undefined — silent validation pass-through. Ensure callback explicitly returns boolean"
|
|
11
|
+
severity: error
|
|
12
|
+
note: |
|
|
13
|
+
UNSAFE:
|
|
14
|
+
z.string().refine((val) => {
|
|
15
|
+
if (val.length > 10) return false;
|
|
16
|
+
// Missing return for other cases → returns undefined → Zod treats as truthy → passes!
|
|
17
|
+
});
|
|
18
|
+
z.string().refine((val) => {
|
|
19
|
+
val.length > 10; // Expression not returned → undefined
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
SAFE:
|
|
23
|
+
z.string().refine((val) => val.length <= 10);
|
|
24
|
+
z.string().refine((val) => {
|
|
25
|
+
if (val.length > 10) return false;
|
|
26
|
+
return true;
|
|
27
|
+
});
|
|
28
|
+
z.string().refine((val) => val.length <= 10, { message: 'Too long' });
|
|
29
|
+
|
|
30
|
+
WHY: When a refine callback uses curly braces (block body) but forgets a return statement,
|
|
31
|
+
it returns undefined. Zod coerces undefined as truthy → the validation ALWAYS passes.
|
|
32
|
+
This means malformed data silently passes through. Senior devs frequently miss the
|
|
33
|
+
return when adding conditional logic to refine callbacks.
|
|
34
|
+
rule:
|
|
35
|
+
any:
|
|
36
|
+
# Block-body arrow function in refine with no return
|
|
37
|
+
- pattern: $SCHEMA.refine(($$$) => { $$$ })
|
|
38
|
+
not:
|
|
39
|
+
any:
|
|
40
|
+
# Safe: expression-body arrow function (implicit return)
|
|
41
|
+
- pattern: $SCHEMA.refine(($$$) => $EXPR)
|
|
42
|
+
# Safe: has explicit return inside
|
|
43
|
+
- pattern: $SCHEMA.refine(($$$) => { $$$ return $$$ $$$ })
|
|
44
|
+
constraints:
|
|
45
|
+
SCHEMA:
|
|
46
|
+
regex: 'z\\..+|[A-Za-z]+'
|
|
47
|
+
ignores:
|
|
48
|
+
- '**/*.test.ts'
|
|
49
|
+
- '**/*.spec.ts'
|
|
50
|
+
- '**/test/**'
|
|
51
|
+
- '**/tests/**'
|
|
52
|
+
- '**/__tests__/**'
|
|
53
|
+
- '**/scripts/**'
|
|
54
|
+
- 'test-*.ts'
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# SOURCE: https://zod.dev/?id=transform
|
|
2
|
+
# "transform() changes the output type. Without chaining an output schema after
|
|
3
|
+
# transform, the inferred type may not match the actual runtime type."
|
|
4
|
+
# SOURCE: https://zod.dev/?id=coercion
|
|
5
|
+
# "When using transform, the output type changes. If you don't validate the
|
|
6
|
+
# transformed value, invalid runtime types bypass type checking."
|
|
7
|
+
# SOURCE: https://zod.dev/?id=chaining-after-transform
|
|
8
|
+
# "After a transform, the type is the transform's return type. Chain a .pipe()
|
|
9
|
+
# with an output schema to validate the transformed result."
|
|
10
|
+
id: zod-transform-without-output-type
|
|
11
|
+
language: typescript
|
|
12
|
+
message: "transform() without chained output schema — transformed value is not validated. Chain .pipe(outputSchema) after transform"
|
|
13
|
+
severity: warning
|
|
14
|
+
note: |
|
|
15
|
+
UNSAFE:
|
|
16
|
+
z.string().transform((val) => JSON.parse(val));
|
|
17
|
+
// If JSON.parse returns unexpected shape, no validation catches it
|
|
18
|
+
|
|
19
|
+
z.string().transform((val) => parseInt(val, 10));
|
|
20
|
+
// NaN passes through without validation
|
|
21
|
+
|
|
22
|
+
SAFE:
|
|
23
|
+
z.string().transform((val) => JSON.parse(val)).pipe(z.object({ name: z.string() }));
|
|
24
|
+
z.string().transform((val) => parseInt(val, 10)).pipe(z.number().int().min(0));
|
|
25
|
+
z.string().pipe(z.coerce.number().int()); // prefer coerce for simple conversions
|
|
26
|
+
|
|
27
|
+
WHY: transform() changes the runtime type but doesn't validate the output. If the
|
|
28
|
+
transform produces an unexpected value (NaN from parseInt, unexpected JSON shape),
|
|
29
|
+
downstream code receives unvalidated data. Senior devs often assume transform()
|
|
30
|
+
implies type safety, but it only changes the type inference — not runtime validation.
|
|
31
|
+
rule:
|
|
32
|
+
any:
|
|
33
|
+
- pattern: $SCHEMA.transform(($$$) => $$$)
|
|
34
|
+
not:
|
|
35
|
+
any:
|
|
36
|
+
# Safe: transform followed by .pipe() with output schema
|
|
37
|
+
- pattern: $SCHEMA.transform(($$$) => $$$).pipe($$$)
|
|
38
|
+
# Safe: transform followed by .refine()
|
|
39
|
+
- pattern: $SCHEMA.transform(($$$) => $$$).refine($$$)
|
|
40
|
+
# Safe: transform followed by another .transform() (chain continues)
|
|
41
|
+
- pattern: $SCHEMA.transform(($$$) => $$$).transform($$$)
|
|
42
|
+
constraints:
|
|
43
|
+
SCHEMA:
|
|
44
|
+
regex: 'z\\..+|[A-Za-z]+'
|
|
45
|
+
ignores:
|
|
46
|
+
- '**/*.test.ts'
|
|
47
|
+
- '**/*.spec.ts'
|
|
48
|
+
- '**/test/**'
|
|
49
|
+
- '**/tests/**'
|
|
50
|
+
- '**/__tests__/**'
|
|
51
|
+
- '**/scripts/**'
|
|
52
|
+
- 'test-*.ts'
|
package/.sg-sha
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
685681f178b4d68d71b671c26912ee2bbb079210
|
package/.sgignore
ADDED
package/AGENTS.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|