@danieljvdm/dev-kit 0.17.0 → 1.0.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 +148 -648
- package/package.json +19 -62
- package/skills/dev-kit/SKILL.md +42 -213
- package/skills/dev-kit/agents/openai.yaml +2 -2
- package/skills/dev-kit/references/cloudflare-worker-api.md +37 -0
- package/skills/dev-kit/references/default-typescript-repository.md +43 -0
- package/skills/dev-kit/references/legacy-eject.md +46 -0
- package/skills/dev-kit/references/repository-setup.md +53 -0
- package/skills/dev-kit/references/skills.md +35 -0
- package/src/bin/dev-kit.ts +153 -134
- package/src/catalog-manager.ts +23 -18
- package/src/catalog.ts +8 -5
- package/src/cli-ui.ts +2 -2
- package/src/effect-tsgo.ts +10 -6
- package/src/eject.ts +715 -0
- package/src/gitignore.ts +6 -3
- package/src/legacy-project.ts +67 -0
- package/src/oxfmt.ts +1 -4
- package/src/oxlint-plugin-anti-slop/LICENSE +21 -0
- package/src/oxlint-plugin-anti-slop/index.ts +43 -0
- package/src/oxlint-plugin-anti-slop/rules/no-chained-type-assertions.ts +79 -0
- package/src/oxlint-plugin-anti-slop/rules/no-conditional-empty-object-spread.ts +51 -0
- package/src/oxlint-plugin-anti-slop/rules/no-known-value-widening.ts +267 -0
- package/src/oxlint-plugin-anti-slop/rules/no-module-mocking.ts +105 -0
- package/src/oxlint-plugin-anti-slop/rules/no-object-parameters.ts +141 -0
- package/src/oxlint-plugin-anti-slop/rules/no-reflect-apply.ts +28 -0
- package/src/oxlint-plugin-anti-slop/rules/no-reflect-get.ts +28 -0
- package/src/oxlint-plugin-anti-slop/rules/no-runtime-typeof.ts +25 -0
- package/src/oxlint-plugin-anti-slop/rules/no-shape-in-symbol-names.ts +38 -0
- package/src/oxlint-plugin-anti-slop/rules/no-unknown-parameters.ts +86 -0
- package/src/oxlint-plugin-anti-slop/rules/no-unknown-returns.ts +125 -0
- package/src/oxlint-plugin-anti-slop/rules/no-unknown-type-aliases.ts +73 -0
- package/src/oxlint-plugin-anti-slop/rules/no-unsafe-dictionary-type.ts +139 -0
- package/src/oxlint-plugin-anti-slop/rules/no-widen-then-assert.ts +396 -0
- package/src/oxlint-plugin-anti-slop/rules/require-safety-comment-for-type-assertion.ts +61 -0
- package/src/oxlint-plugin-anti-slop/runtime.js +1209 -0
- package/src/oxlint-plugin-anti-slop/shared/dictionary-types.ts +555 -0
- package/src/oxlint-plugin-anti-slop/shared/reflect-method.ts +40 -0
- package/src/oxlint.ts +26 -14
- package/src/package-skill-source.ts +17 -25
- package/src/path-digest.ts +62 -1
- package/src/project-package.ts +14 -12
- package/src/project-skills.ts +722 -0
- package/src/tool-metadata.ts +0 -2
- package/src/vendor.ts +35 -21
- package/src/vite-plus.ts +1 -3
- package/dev-kit.example.jsonc +0 -22
- package/schema/dev-kit.schema.json +0 -218
- package/schema/skill-sources.schema.json +0 -83
- package/src/index.ts +0 -125
- package/src/manifest.ts +0 -216
- package/src/oxfmt.js +0 -23
- package/src/oxlint-plugin-effect.d.ts +0 -17
- package/src/oxlint-plugin-style.d.ts +0 -8
- package/src/oxlint.js +0 -94
- package/src/project-state.ts +0 -122
- package/src/scaffold.ts +0 -79
- package/src/skill-manager.ts +0 -515
- package/src/sync.ts +0 -1919
- package/src/tool-ignore-patterns.js +0 -9
- package/src/vite-plus-dependency.ts +0 -69
- package/src/vite-plus-hooks.ts +0 -175
- package/src/vite-plus-workflow.ts +0 -82
- package/src/vite-plus.js +0 -88
- package/src/worktrunk-config.ts +0 -88
- package/templates/AGENTS.md +0 -11
- package/templates/vite-plus/github-actions-check.yml +0 -51
- package/templates/worktrunk/wt.toml +0 -27
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { defineRule, type ESTree, type SourceCode } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
type Parameter = ESTree.ParamPattern;
|
|
4
|
+
type ParameterOwner =
|
|
5
|
+
| ESTree.ArrowFunctionExpression
|
|
6
|
+
| ESTree.Function
|
|
7
|
+
| ESTree.TSCallSignatureDeclaration
|
|
8
|
+
| ESTree.TSConstructSignatureDeclaration
|
|
9
|
+
| ESTree.TSConstructorType
|
|
10
|
+
| ESTree.TSFunctionType
|
|
11
|
+
| ESTree.TSMethodSignature;
|
|
12
|
+
|
|
13
|
+
function parameterAnnotation(parameter: Parameter): ESTree.TSTypeAnnotation | null | undefined {
|
|
14
|
+
if (parameter.type === "TSParameterProperty") {
|
|
15
|
+
return parameterAnnotation(parameter.parameter);
|
|
16
|
+
}
|
|
17
|
+
if (parameter.type === "RestElement") {
|
|
18
|
+
return parameter.typeAnnotation ?? parameterAnnotation(parameter.argument);
|
|
19
|
+
}
|
|
20
|
+
if (parameter.type === "AssignmentPattern") {
|
|
21
|
+
return parameter.typeAnnotation ?? parameter.left.typeAnnotation;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return parameter.typeAnnotation;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parameterName(parameter: Parameter, sourceCode: SourceCode): string {
|
|
28
|
+
return parameter.type === "Identifier"
|
|
29
|
+
? parameter.name
|
|
30
|
+
: sourceCode.getText(parameter).replace(/\s*:\s*object\s*$/u, "");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function lexicalTypeParameterNames(node: ESTree.Node): ReadonlySet<string> {
|
|
34
|
+
const names = new Set<string>();
|
|
35
|
+
let current: ESTree.Node | null = node;
|
|
36
|
+
|
|
37
|
+
while (current !== null && current.type !== "Program") {
|
|
38
|
+
if ("typeParameters" in current) {
|
|
39
|
+
for (const parameter of current.typeParameters?.params ?? []) {
|
|
40
|
+
names.add(parameter.name.name);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (current.type === "TSMappedType") names.add(current.key.name);
|
|
44
|
+
if (current.type === "TSInferType") names.add(current.typeParameter.name.name);
|
|
45
|
+
current = current.parent;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return names;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Ban the broad object type on function inputs, including local aliases to object. */
|
|
52
|
+
export const noObjectParametersRule = defineRule({
|
|
53
|
+
meta: {
|
|
54
|
+
type: "problem",
|
|
55
|
+
docs: {
|
|
56
|
+
description:
|
|
57
|
+
"Disallow object function parameters; inputs must use an owner-provided type and be parsed at their boundary.",
|
|
58
|
+
},
|
|
59
|
+
messages: {
|
|
60
|
+
objectParameter:
|
|
61
|
+
"Parameter `{{parameter}}` uses the broad `object` type. Accept a named owner type; parse external input at its boundary before calling this function.",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
create(context) {
|
|
65
|
+
const aliases = new Map<string, ESTree.TSType>();
|
|
66
|
+
|
|
67
|
+
const resolvesToObject = (
|
|
68
|
+
type: ESTree.TSType,
|
|
69
|
+
shadowedAliases: ReadonlySet<string>,
|
|
70
|
+
visited = new Set<string>(),
|
|
71
|
+
): boolean => {
|
|
72
|
+
if (type.type === "TSObjectKeyword") return true;
|
|
73
|
+
if (type.type === "TSParenthesizedType")
|
|
74
|
+
return resolvesToObject(type.typeAnnotation, shadowedAliases, visited);
|
|
75
|
+
if (type.type === "TSUnionType") {
|
|
76
|
+
return type.types.some((member) => resolvesToObject(member, shadowedAliases, visited));
|
|
77
|
+
}
|
|
78
|
+
if (
|
|
79
|
+
type.type !== "TSTypeReference" ||
|
|
80
|
+
type.typeName.type !== "Identifier" ||
|
|
81
|
+
(type.typeArguments !== null &&
|
|
82
|
+
type.typeArguments !== undefined &&
|
|
83
|
+
type.typeArguments.params.length > 0) ||
|
|
84
|
+
visited.has(type.typeName.name) ||
|
|
85
|
+
shadowedAliases.has(type.typeName.name)
|
|
86
|
+
) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const alias = aliases.get(type.typeName.name);
|
|
90
|
+
|
|
91
|
+
if (alias === undefined) return false;
|
|
92
|
+
const nextVisited = new Set(visited);
|
|
93
|
+
|
|
94
|
+
nextVisited.add(type.typeName.name);
|
|
95
|
+
|
|
96
|
+
return resolvesToObject(alias, shadowedAliases, nextVisited);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const checkParameters = (node: ParameterOwner) => {
|
|
100
|
+
const shadowedAliases = lexicalTypeParameterNames(node);
|
|
101
|
+
|
|
102
|
+
for (const parameter of node.params) {
|
|
103
|
+
const annotation = parameterAnnotation(parameter);
|
|
104
|
+
|
|
105
|
+
if (annotation === null || annotation === undefined) continue;
|
|
106
|
+
if (!resolvesToObject(annotation.typeAnnotation, shadowedAliases)) continue;
|
|
107
|
+
context.report({
|
|
108
|
+
node: annotation.typeAnnotation,
|
|
109
|
+
messageId: "objectParameter",
|
|
110
|
+
data: { parameter: parameterName(parameter, context.sourceCode) },
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
Program(node) {
|
|
117
|
+
for (const statement of node.body) {
|
|
118
|
+
const declaration =
|
|
119
|
+
statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
declaration?.type === "TSTypeAliasDeclaration" &&
|
|
123
|
+
(declaration.typeParameters === null || declaration.typeParameters === undefined)
|
|
124
|
+
) {
|
|
125
|
+
aliases.set(declaration.id.name, declaration.typeAnnotation);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
ArrowFunctionExpression: checkParameters,
|
|
130
|
+
FunctionDeclaration: checkParameters,
|
|
131
|
+
FunctionExpression: checkParameters,
|
|
132
|
+
TSCallSignatureDeclaration: checkParameters,
|
|
133
|
+
TSConstructSignatureDeclaration: checkParameters,
|
|
134
|
+
TSConstructorType: checkParameters,
|
|
135
|
+
TSDeclareFunction: checkParameters,
|
|
136
|
+
TSEmptyBodyFunctionExpression: checkParameters,
|
|
137
|
+
TSFunctionType: checkParameters,
|
|
138
|
+
TSMethodSignature: checkParameters,
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import { isGlobalReflectMethodCall } from "../shared/reflect-method.ts";
|
|
4
|
+
|
|
5
|
+
/** Ban Reflect.apply, which bypasses ordinary typed function calls. */
|
|
6
|
+
export const noReflectApplyRule = defineRule({
|
|
7
|
+
meta: {
|
|
8
|
+
type: "problem",
|
|
9
|
+
docs: {
|
|
10
|
+
description:
|
|
11
|
+
"Disallow Reflect.apply; call typed functions directly or model dynamic dispatch behind an interface.",
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
reflectApply:
|
|
15
|
+
"Replace `Reflect.apply` with a typed function call. Model dynamic dispatch behind a named interface.",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
return {
|
|
20
|
+
CallExpression(node) {
|
|
21
|
+
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
|
|
22
|
+
if (isGlobalReflectMethodCall(context.sourceCode, node.callee, "apply")) {
|
|
23
|
+
context.report({ node, messageId: "reflectApply" });
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import { isGlobalReflectMethodCall } from "../shared/reflect-method.ts";
|
|
4
|
+
|
|
5
|
+
/** Ban Reflect.get, which bypasses ordinary property access and useful type evidence. */
|
|
6
|
+
export const noReflectGetRule = defineRule({
|
|
7
|
+
meta: {
|
|
8
|
+
type: "problem",
|
|
9
|
+
docs: {
|
|
10
|
+
description:
|
|
11
|
+
"Disallow Reflect.get; use typed property access or parse dynamic input into a domain type.",
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
reflectGet:
|
|
15
|
+
"Replace `Reflect.get` with typed property access. Parse dynamic input into a named domain type before reading it.",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
return {
|
|
20
|
+
CallExpression(node) {
|
|
21
|
+
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
|
|
22
|
+
if (isGlobalReflectMethodCall(context.sourceCode, node.callee, "get")) {
|
|
23
|
+
context.report({ node, messageId: "reflectGet" });
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
/** Disallow runtime typeof checks that narrow unparsed values instead of decoding them. */
|
|
4
|
+
export const noRuntimeTypeofRule = defineRule({
|
|
5
|
+
meta: {
|
|
6
|
+
type: "problem",
|
|
7
|
+
docs: {
|
|
8
|
+
description:
|
|
9
|
+
"Disallow runtime typeof checks; external values must be decoded into meaningful types at their I/O boundary.",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
runtimeTypeof:
|
|
13
|
+
"A `typeof` check narrows a representation without establishing its contract. Parse input at its I/O boundary, then branch on the domain value.",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
UnaryExpression(node) {
|
|
19
|
+
if (node.operator === "typeof") {
|
|
20
|
+
context.report({ node, messageId: "runtimeTypeof" });
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { defineRule, type ESTree } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
const FORBIDDEN_SYMBOL_NAME = "shape";
|
|
4
|
+
|
|
5
|
+
function containsForbiddenSymbolName(name: string): boolean {
|
|
6
|
+
return name.toLowerCase().includes(FORBIDDEN_SYMBOL_NAME);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Ban the case-insensitive substring "shape" in every JavaScript and TypeScript symbol name. */
|
|
10
|
+
export const noForbiddenTermInSymbolNamesRule = defineRule({
|
|
11
|
+
meta: {
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description:
|
|
15
|
+
'Disallow the case-insensitive substring "shape" in JavaScript, TypeScript, private, and JSX symbol names.',
|
|
16
|
+
},
|
|
17
|
+
messages: {
|
|
18
|
+
forbiddenSymbolName:
|
|
19
|
+
'Rename symbol "{{name}}" for its domain role; "shape" describes structure rather than ownership.',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
create(context) {
|
|
23
|
+
const reportForbiddenSymbolName = (node: ESTree.Node & { name: string }) => {
|
|
24
|
+
if (!containsForbiddenSymbolName(node.name)) return;
|
|
25
|
+
context.report({
|
|
26
|
+
node,
|
|
27
|
+
messageId: "forbiddenSymbolName",
|
|
28
|
+
data: { name: node.name },
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
Identifier: reportForbiddenSymbolName,
|
|
34
|
+
PrivateIdentifier: reportForbiddenSymbolName,
|
|
35
|
+
JSXIdentifier: reportForbiddenSymbolName,
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { defineRule, type ESTree } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
type Parameter = ESTree.ParamPattern;
|
|
4
|
+
type ParameterOwner =
|
|
5
|
+
| ESTree.ArrowFunctionExpression
|
|
6
|
+
| ESTree.Function
|
|
7
|
+
| ESTree.TSCallSignatureDeclaration
|
|
8
|
+
| ESTree.TSConstructSignatureDeclaration
|
|
9
|
+
| ESTree.TSConstructorType
|
|
10
|
+
| ESTree.TSFunctionType
|
|
11
|
+
| ESTree.TSMethodSignature;
|
|
12
|
+
|
|
13
|
+
function parameterAnnotation(parameter: Parameter): ESTree.TSTypeAnnotation | null | undefined {
|
|
14
|
+
if (parameter.type === "TSParameterProperty") {
|
|
15
|
+
return parameterAnnotation(parameter.parameter);
|
|
16
|
+
}
|
|
17
|
+
if (parameter.type === "RestElement") {
|
|
18
|
+
return parameter.typeAnnotation ?? parameterAnnotation(parameter.argument);
|
|
19
|
+
}
|
|
20
|
+
if (parameter.type === "AssignmentPattern") {
|
|
21
|
+
return parameter.typeAnnotation ?? parameter.left.typeAnnotation;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return parameter.typeAnnotation;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parameterName(parameter: Parameter, sourceText: string): string {
|
|
28
|
+
if (parameter.type === "TSParameterProperty") {
|
|
29
|
+
return parameterName(parameter.parameter, sourceText);
|
|
30
|
+
}
|
|
31
|
+
if (parameter.type === "AssignmentPattern") {
|
|
32
|
+
return parameterName(parameter.left, sourceText);
|
|
33
|
+
}
|
|
34
|
+
if (parameter.type === "RestElement") {
|
|
35
|
+
return parameterName(parameter.argument, sourceText);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return parameter.type === "Identifier"
|
|
39
|
+
? parameter.name
|
|
40
|
+
: sourceText.replace(/\s*:\s*unknown\s*$/u, "");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Disallow unknown inputs except explicitly named error-cause enrichment. */
|
|
44
|
+
export const noUnknownParametersRule = defineRule({
|
|
45
|
+
meta: {
|
|
46
|
+
type: "problem",
|
|
47
|
+
docs: {
|
|
48
|
+
description:
|
|
49
|
+
"Disallow explicitly unknown function parameters except `cause`; decode unknown input at its I/O boundary instead.",
|
|
50
|
+
},
|
|
51
|
+
messages: {
|
|
52
|
+
unknownParameter:
|
|
53
|
+
"Parameter `{{parameter}}` leaves input unparsed. Accept a named domain type; run the expected schema or parser at the I/O boundary before calling this function.",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
create(context) {
|
|
57
|
+
const checkParameters = (node: ParameterOwner) => {
|
|
58
|
+
for (const parameter of node.params) {
|
|
59
|
+
const annotation = parameterAnnotation(parameter);
|
|
60
|
+
|
|
61
|
+
if (annotation?.typeAnnotation.type !== "TSUnknownKeyword") continue;
|
|
62
|
+
const name = parameterName(parameter, context.sourceCode.getText(parameter));
|
|
63
|
+
|
|
64
|
+
if (name === "cause") continue;
|
|
65
|
+
context.report({
|
|
66
|
+
node: annotation.typeAnnotation,
|
|
67
|
+
messageId: "unknownParameter",
|
|
68
|
+
data: { parameter: name },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
ArrowFunctionExpression: checkParameters,
|
|
75
|
+
FunctionDeclaration: checkParameters,
|
|
76
|
+
FunctionExpression: checkParameters,
|
|
77
|
+
TSCallSignatureDeclaration: checkParameters,
|
|
78
|
+
TSConstructSignatureDeclaration: checkParameters,
|
|
79
|
+
TSConstructorType: checkParameters,
|
|
80
|
+
TSDeclareFunction: checkParameters,
|
|
81
|
+
TSEmptyBodyFunctionExpression: checkParameters,
|
|
82
|
+
TSFunctionType: checkParameters,
|
|
83
|
+
TSMethodSignature: checkParameters,
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { defineRule, type ESTree } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
type FunctionWithReturnType =
|
|
4
|
+
| ESTree.ArrowFunctionExpression
|
|
5
|
+
| ESTree.Function
|
|
6
|
+
| ESTree.TSCallSignatureDeclaration
|
|
7
|
+
| ESTree.TSConstructSignatureDeclaration
|
|
8
|
+
| ESTree.TSConstructorType
|
|
9
|
+
| ESTree.TSFunctionType
|
|
10
|
+
| ESTree.TSMethodSignature;
|
|
11
|
+
|
|
12
|
+
function referencedAliasName(type: ESTree.TSType): string | null {
|
|
13
|
+
if (type.type === "TSParenthesizedType") return referencedAliasName(type.typeAnnotation);
|
|
14
|
+
if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier") return null;
|
|
15
|
+
|
|
16
|
+
return type.typeArguments === null ||
|
|
17
|
+
type.typeArguments === undefined ||
|
|
18
|
+
type.typeArguments.params.length === 0
|
|
19
|
+
? type.typeName.name
|
|
20
|
+
: null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function lexicalTypeParameterNames(node: ESTree.Node): ReadonlySet<string> {
|
|
24
|
+
const names = new Set<string>();
|
|
25
|
+
let current: ESTree.Node | null = node;
|
|
26
|
+
|
|
27
|
+
while (current !== null && current.type !== "Program") {
|
|
28
|
+
if ("typeParameters" in current) {
|
|
29
|
+
for (const parameter of current.typeParameters?.params ?? []) {
|
|
30
|
+
names.add(parameter.name.name);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
current = current.parent;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return names;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Ban function contracts that return unknown instead of a parsed domain type. */
|
|
40
|
+
export const noUnknownReturnsRule = defineRule({
|
|
41
|
+
meta: {
|
|
42
|
+
type: "problem",
|
|
43
|
+
docs: {
|
|
44
|
+
description:
|
|
45
|
+
"Disallow functions whose explicit return contract is unknown or Promise<unknown>.",
|
|
46
|
+
},
|
|
47
|
+
messages: {
|
|
48
|
+
unknownReturn:
|
|
49
|
+
"This function exposes `unknown` to its caller. Parse the value at its boundary and return a named domain type.",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
create(context) {
|
|
53
|
+
const aliases = new Map<string, ESTree.TSTypeAliasDeclaration>();
|
|
54
|
+
|
|
55
|
+
const resolvesToUnknown = (
|
|
56
|
+
type: ESTree.TSType,
|
|
57
|
+
shadowedAliases: ReadonlySet<string>,
|
|
58
|
+
visited = new Set<string>(),
|
|
59
|
+
): boolean => {
|
|
60
|
+
if (type.type === "TSUnknownKeyword") return true;
|
|
61
|
+
if (type.type === "TSParenthesizedType") {
|
|
62
|
+
return resolvesToUnknown(type.typeAnnotation, shadowedAliases, visited);
|
|
63
|
+
}
|
|
64
|
+
if (type.type === "TSUnionType") {
|
|
65
|
+
return type.types.some((member) => resolvesToUnknown(member, shadowedAliases, visited));
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
type.type === "TSTypeReference" &&
|
|
69
|
+
type.typeName.type === "Identifier" &&
|
|
70
|
+
(type.typeName.name === "Promise" || type.typeName.name === "PromiseLike")
|
|
71
|
+
) {
|
|
72
|
+
const value = type.typeArguments?.params[0];
|
|
73
|
+
|
|
74
|
+
return value !== undefined && resolvesToUnknown(value, shadowedAliases, visited);
|
|
75
|
+
}
|
|
76
|
+
const name = referencedAliasName(type);
|
|
77
|
+
|
|
78
|
+
if (name === null || visited.has(name) || shadowedAliases.has(name)) return false;
|
|
79
|
+
const alias = aliases.get(name);
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
alias === undefined ||
|
|
83
|
+
(alias.typeParameters !== null && alias.typeParameters !== undefined)
|
|
84
|
+
) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const nextVisited = new Set(visited);
|
|
88
|
+
|
|
89
|
+
nextVisited.add(name);
|
|
90
|
+
|
|
91
|
+
return resolvesToUnknown(alias.typeAnnotation, shadowedAliases, nextVisited);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const checkReturnType = (node: FunctionWithReturnType) => {
|
|
95
|
+
const annotation = node.returnType;
|
|
96
|
+
|
|
97
|
+
if (annotation === null || annotation === undefined) return;
|
|
98
|
+
if (!resolvesToUnknown(annotation.typeAnnotation, lexicalTypeParameterNames(node))) return;
|
|
99
|
+
context.report({ node: annotation.typeAnnotation, messageId: "unknownReturn" });
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
Program(node) {
|
|
104
|
+
for (const statement of node.body) {
|
|
105
|
+
const declaration =
|
|
106
|
+
statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
107
|
+
|
|
108
|
+
if (declaration?.type === "TSTypeAliasDeclaration") {
|
|
109
|
+
aliases.set(declaration.id.name, declaration);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
ArrowFunctionExpression: checkReturnType,
|
|
114
|
+
FunctionDeclaration: checkReturnType,
|
|
115
|
+
FunctionExpression: checkReturnType,
|
|
116
|
+
TSCallSignatureDeclaration: checkReturnType,
|
|
117
|
+
TSConstructSignatureDeclaration: checkReturnType,
|
|
118
|
+
TSConstructorType: checkReturnType,
|
|
119
|
+
TSDeclareFunction: checkReturnType,
|
|
120
|
+
TSEmptyBodyFunctionExpression: checkReturnType,
|
|
121
|
+
TSFunctionType: checkReturnType,
|
|
122
|
+
TSMethodSignature: checkReturnType,
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { defineRule, type ESTree } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
function referencedAliasName(type: ESTree.TSType): string | null {
|
|
4
|
+
if (type.type === "TSParenthesizedType") return referencedAliasName(type.typeAnnotation);
|
|
5
|
+
if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier") return null;
|
|
6
|
+
|
|
7
|
+
return type.typeArguments === null ||
|
|
8
|
+
type.typeArguments === undefined ||
|
|
9
|
+
type.typeArguments.params.length === 0
|
|
10
|
+
? type.typeName.name
|
|
11
|
+
: null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Ban named aliases that merely conceal TypeScript's unknown top type. */
|
|
15
|
+
export const noUnknownTypeAliasesRule = defineRule({
|
|
16
|
+
meta: {
|
|
17
|
+
type: "problem",
|
|
18
|
+
docs: {
|
|
19
|
+
description:
|
|
20
|
+
"Disallow type aliases whose resolved type is unknown; unknown must remain visible at an allowed boundary.",
|
|
21
|
+
},
|
|
22
|
+
messages: {
|
|
23
|
+
unknownAlias:
|
|
24
|
+
"Type alias `{{alias}}` hides `unknown`. Keep `unknown` explicit at the parsing boundary or on an allowed `cause` field; otherwise use the parsed owner type.",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
create(context) {
|
|
28
|
+
const aliases = new Map<string, ESTree.TSTypeAliasDeclaration>();
|
|
29
|
+
|
|
30
|
+
const resolvesToUnknown = (type: ESTree.TSType, visited = new Set<string>()): boolean => {
|
|
31
|
+
if (type.type === "TSUnknownKeyword") return true;
|
|
32
|
+
if (type.type === "TSParenthesizedType")
|
|
33
|
+
return resolvesToUnknown(type.typeAnnotation, visited);
|
|
34
|
+
const name = referencedAliasName(type);
|
|
35
|
+
|
|
36
|
+
if (name === null || visited.has(name)) return false;
|
|
37
|
+
const alias = aliases.get(name);
|
|
38
|
+
|
|
39
|
+
if (
|
|
40
|
+
alias === undefined ||
|
|
41
|
+
(alias.typeParameters !== null && alias.typeParameters !== undefined)
|
|
42
|
+
) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const nextVisited = new Set(visited);
|
|
46
|
+
|
|
47
|
+
nextVisited.add(name);
|
|
48
|
+
|
|
49
|
+
return resolvesToUnknown(alias.typeAnnotation, nextVisited);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
Program(node) {
|
|
54
|
+
for (const statement of node.body) {
|
|
55
|
+
const declaration =
|
|
56
|
+
statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
57
|
+
|
|
58
|
+
if (declaration?.type === "TSTypeAliasDeclaration") {
|
|
59
|
+
aliases.set(declaration.id.name, declaration);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (const alias of aliases.values()) {
|
|
63
|
+
if (!resolvesToUnknown(alias.typeAnnotation, new Set([alias.id.name]))) continue;
|
|
64
|
+
context.report({
|
|
65
|
+
node: alias.id,
|
|
66
|
+
messageId: "unknownAlias",
|
|
67
|
+
data: { alias: alias.id.name },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { defineRule, type ESTree } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
classifyUnsafeDictionary,
|
|
5
|
+
classifyUnsafeDictionaryValue,
|
|
6
|
+
createTypeEnvironment,
|
|
7
|
+
type TypeEnvironment,
|
|
8
|
+
} from "../shared/dictionary-types.ts";
|
|
9
|
+
|
|
10
|
+
const typeNodeKinds: ReadonlySet<string> = new Set([
|
|
11
|
+
"JSDocNonNullableType",
|
|
12
|
+
"JSDocNullableType",
|
|
13
|
+
"JSDocUnknownType",
|
|
14
|
+
"TSAnyKeyword",
|
|
15
|
+
"TSArrayType",
|
|
16
|
+
"TSBigIntKeyword",
|
|
17
|
+
"TSBooleanKeyword",
|
|
18
|
+
"TSConditionalType",
|
|
19
|
+
"TSConstructorType",
|
|
20
|
+
"TSFunctionType",
|
|
21
|
+
"TSImportType",
|
|
22
|
+
"TSIndexedAccessType",
|
|
23
|
+
"TSInferType",
|
|
24
|
+
"TSIntersectionType",
|
|
25
|
+
"TSIntrinsicKeyword",
|
|
26
|
+
"TSLiteralType",
|
|
27
|
+
"TSMappedType",
|
|
28
|
+
"TSNamedTupleMember",
|
|
29
|
+
"TSNeverKeyword",
|
|
30
|
+
"TSNullKeyword",
|
|
31
|
+
"TSNumberKeyword",
|
|
32
|
+
"TSObjectKeyword",
|
|
33
|
+
"TSParenthesizedType",
|
|
34
|
+
"TSStringKeyword",
|
|
35
|
+
"TSSymbolKeyword",
|
|
36
|
+
"TSTemplateLiteralType",
|
|
37
|
+
"TSThisType",
|
|
38
|
+
"TSTupleType",
|
|
39
|
+
"TSTypeLiteral",
|
|
40
|
+
"TSTypeOperator",
|
|
41
|
+
"TSTypePredicate",
|
|
42
|
+
"TSTypeQuery",
|
|
43
|
+
"TSTypeReference",
|
|
44
|
+
"TSUndefinedKeyword",
|
|
45
|
+
"TSUnionType",
|
|
46
|
+
"TSUnknownKeyword",
|
|
47
|
+
"TSVoidKeyword",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
function isTypeNode(node: ESTree.Node): node is ESTree.TSType {
|
|
51
|
+
return typeNodeKinds.has(node.type);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function typeReferenceName(type: ESTree.TSTypeReference): string | null {
|
|
55
|
+
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isInsideTypeAliasDeclaration(node: ESTree.Node): boolean {
|
|
59
|
+
let current: ESTree.Node | null = node.parent;
|
|
60
|
+
|
|
61
|
+
while (current !== null && current.type !== "Program") {
|
|
62
|
+
if (current.type === "TSTypeAliasDeclaration") return true;
|
|
63
|
+
current = current.parent;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isPlainAliasConsumerUse(node: ESTree.TSType, environment: TypeEnvironment): boolean {
|
|
70
|
+
if (node.type !== "TSTypeReference" || node.typeArguments?.params.length) return false;
|
|
71
|
+
const name = typeReferenceName(node);
|
|
72
|
+
|
|
73
|
+
return name !== null && environment.aliases.has(name) && !isInsideTypeAliasDeclaration(node);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function shouldReportType(node: ESTree.TSType, environment: TypeEnvironment): boolean {
|
|
77
|
+
if (isPlainAliasConsumerUse(node, environment)) return false;
|
|
78
|
+
if (classifyUnsafeDictionary(node, environment) === null) return false;
|
|
79
|
+
let current: ESTree.Node | null = node.parent;
|
|
80
|
+
|
|
81
|
+
while (current !== null && current.type !== "Program") {
|
|
82
|
+
if (isTypeNode(current) && classifyUnsafeDictionary(current, environment) !== null)
|
|
83
|
+
return false;
|
|
84
|
+
current = current.parent;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Disallow object-dictionary contracts whose direct value type is an unsafe escape hatch. */
|
|
91
|
+
export const noUnsafeDictionaryTypeRule = defineRule({
|
|
92
|
+
meta: {
|
|
93
|
+
type: "problem",
|
|
94
|
+
docs: {
|
|
95
|
+
description:
|
|
96
|
+
"Disallow object-dictionary contracts whose direct value type is unknown, any, object, {}, or a union/alias containing one of those escape hatches.",
|
|
97
|
+
},
|
|
98
|
+
messages: {
|
|
99
|
+
unsafeDictionary:
|
|
100
|
+
"This dictionary's {{value}} value type gives callers no concrete value contract. Use an owner/schema-derived value type; parse external payloads before insertion.",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
createOnce(context) {
|
|
104
|
+
let environment: TypeEnvironment | null = null;
|
|
105
|
+
const report = (node: ESTree.Node, value: string) => {
|
|
106
|
+
context.report({ node, messageId: "unsafeDictionary", data: { value } });
|
|
107
|
+
};
|
|
108
|
+
const reportIfUnsafe = (node: ESTree.TSType) => {
|
|
109
|
+
if (environment === null || !shouldReportType(node, environment)) return;
|
|
110
|
+
const unsafe = classifyUnsafeDictionary(node, environment);
|
|
111
|
+
|
|
112
|
+
if (unsafe === null) return;
|
|
113
|
+
report(node, unsafe.unsafeValue);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
Program(node) {
|
|
118
|
+
environment = createTypeEnvironment(node);
|
|
119
|
+
},
|
|
120
|
+
TSTypeReference: reportIfUnsafe,
|
|
121
|
+
TSTypeLiteral: reportIfUnsafe,
|
|
122
|
+
TSMappedType: reportIfUnsafe,
|
|
123
|
+
TSIndexSignature(node) {
|
|
124
|
+
if (
|
|
125
|
+
environment === null ||
|
|
126
|
+
node.typeAnnotation === null ||
|
|
127
|
+
node.parent.type === "TSTypeLiteral"
|
|
128
|
+
)
|
|
129
|
+
return;
|
|
130
|
+
const unsafe = classifyUnsafeDictionaryValue(
|
|
131
|
+
node.typeAnnotation.typeAnnotation,
|
|
132
|
+
environment,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (unsafe !== null) report(node, unsafe.unsafeValue);
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
});
|