@configg/oxc 0.1.5 → 0.1.6
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 +5 -0
- package/package.json +4 -1
- package/src/anti-slop/LICENSE +21 -0
- package/src/anti-slop/UPSTREAM.md +8 -0
- package/src/anti-slop/index.ts +47 -0
- package/src/anti-slop/rules/no-array-filter-map.ts +28 -0
- package/src/anti-slop/rules/no-chained-type-assertions.ts +77 -0
- package/src/anti-slop/rules/no-conditional-empty-object-spread.ts +49 -0
- package/src/anti-slop/rules/no-known-value-widening.ts +415 -0
- package/src/anti-slop/rules/no-module-mocking.ts +80 -0
- package/src/anti-slop/rules/no-object-parameters.ts +83 -0
- package/src/anti-slop/rules/no-reduce-accumulator-copy.ts +109 -0
- package/src/anti-slop/rules/no-reflect-apply.ts +28 -0
- package/src/anti-slop/rules/no-reflect-get.ts +28 -0
- package/src/anti-slop/rules/no-runtime-typeof.ts +77 -0
- package/src/anti-slop/rules/no-shape-in-symbol-names.ts +46 -0
- package/src/anti-slop/rules/no-unknown-parameters.ts +69 -0
- package/src/anti-slop/rules/no-unknown-returns.ts +82 -0
- package/src/anti-slop/rules/no-unknown-type-aliases.ts +54 -0
- package/src/anti-slop/rules/no-unsafe-dictionary-type.ts +154 -0
- package/src/anti-slop/rules/no-widen-then-assert.ts +366 -0
- package/src/anti-slop/rules/require-readable-spacing.ts +47 -0
- package/src/anti-slop/rules/require-safety-comment-for-type-assertion.ts +131 -0
- package/src/anti-slop/shared/array-method.ts +94 -0
- package/src/anti-slop/shared/dictionary-types.ts +515 -0
- package/src/anti-slop/shared/function-parameters.ts +49 -0
- package/src/anti-slop/shared/lexical-type-parameters.ts +61 -0
- package/src/anti-slop/shared/reflect-method.ts +24 -0
- package/src/anti-slop/shared/scope.ts +15 -0
- package/src/anti-slop/shared/type-alias-resolution.ts +250 -0
- package/src/anti-slop/vendor/eslint-stylistic/LICENSE +22 -0
- package/src/anti-slop/vendor/eslint-stylistic/UPSTREAM.md +28 -0
- package/src/anti-slop/vendor/eslint-stylistic/padding-line-ast.ts +51 -0
- package/src/anti-slop/vendor/eslint-stylistic/padding-line-between-statements.ts +906 -0
- package/src/anti-slop/vendor/eslint-stylistic/padding-line-options.d.ts +87 -0
- package/src/lint.ts +26 -2
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import { resolveVariable } from "../shared/scope.ts";
|
|
4
|
+
|
|
5
|
+
import type { ESTree, SourceCode } from "@oxlint/plugins";
|
|
6
|
+
|
|
7
|
+
const moduleMockMethods = new Set(["doMock", "mock", "unstable_mockModule"]);
|
|
8
|
+
|
|
9
|
+
function importedName(node: ESTree.Node): string | null {
|
|
10
|
+
if (node.type !== "ImportSpecifier") return null;
|
|
11
|
+
return node.imported.type === "Identifier" ? node.imported.name : node.imported.value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isTestFrameworkObject(
|
|
15
|
+
sourceCode: SourceCode,
|
|
16
|
+
expression: ESTree.Expression,
|
|
17
|
+
): expression is ESTree.IdentifierReference {
|
|
18
|
+
if (expression.type !== "Identifier") return false;
|
|
19
|
+
if (
|
|
20
|
+
(expression.name === "vi" || expression.name === "jest") &&
|
|
21
|
+
sourceCode.isGlobalReference(expression)
|
|
22
|
+
) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const variable = resolveVariable(sourceCode, expression);
|
|
27
|
+
if (variable === null || variable.defs.length === 0) {
|
|
28
|
+
return expression.name === "vi" || expression.name === "jest";
|
|
29
|
+
}
|
|
30
|
+
return variable.defs.some((definition) => {
|
|
31
|
+
if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration") {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const source = definition.parent.source.value;
|
|
35
|
+
const name = importedName(definition.node);
|
|
36
|
+
return (source === "vitest" && name === "vi") || (source === "@jest/globals" && name === "jest");
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function moduleMockCall(sourceCode: SourceCode, callee: ESTree.Expression): boolean {
|
|
41
|
+
if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
|
|
42
|
+
if (!isTestFrameworkObject(sourceCode, callee.object)) return false;
|
|
43
|
+
const property = callee.property;
|
|
44
|
+
const method = callee.computed
|
|
45
|
+
? property.type === "Literal" &&
|
|
46
|
+
(property.value === "doMock" ||
|
|
47
|
+
property.value === "mock" ||
|
|
48
|
+
property.value === "unstable_mockModule")
|
|
49
|
+
? property.value
|
|
50
|
+
: null
|
|
51
|
+
: property.type === "Identifier"
|
|
52
|
+
? property.name
|
|
53
|
+
: null;
|
|
54
|
+
return method !== null && moduleMockMethods.has(method);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Ban test framework module mocking in favor of real dependency seams. */
|
|
58
|
+
export const noModuleMockingRule = defineRule({
|
|
59
|
+
meta: {
|
|
60
|
+
type: "problem",
|
|
61
|
+
docs: {
|
|
62
|
+
description:
|
|
63
|
+
"Disallow Vitest and Jest module mocking; tests must replace dependencies through real interfaces.",
|
|
64
|
+
},
|
|
65
|
+
messages: {
|
|
66
|
+
moduleMock:
|
|
67
|
+
"Replace module mocking with dependency injection through a real interface, service layer, or faithful test implementation.",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
createOnce(context) {
|
|
71
|
+
return {
|
|
72
|
+
CallExpression(node) {
|
|
73
|
+
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
|
|
74
|
+
if (moduleMockCall(context.sourceCode, node.callee)) {
|
|
75
|
+
context.report({ node, messageId: "moduleMock" });
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
functionParameterBindingName,
|
|
7
|
+
functionParameterTypeAnnotation,
|
|
8
|
+
} from "../shared/function-parameters.ts";
|
|
9
|
+
import {
|
|
10
|
+
createTypeAliasEnvironment,
|
|
11
|
+
resolvedTypeMatches,
|
|
12
|
+
type TypeAliasEnvironment,
|
|
13
|
+
} from "../shared/type-alias-resolution.ts";
|
|
14
|
+
type ParameterOwner =
|
|
15
|
+
| ESTree.ArrowFunctionExpression
|
|
16
|
+
| ESTree.Function
|
|
17
|
+
| ESTree.TSCallSignatureDeclaration
|
|
18
|
+
| ESTree.TSConstructSignatureDeclaration
|
|
19
|
+
| ESTree.TSConstructorType
|
|
20
|
+
| ESTree.TSFunctionType
|
|
21
|
+
| ESTree.TSMethodSignature;
|
|
22
|
+
|
|
23
|
+
/** Ban the broad object type on function inputs, including local aliases to object. */
|
|
24
|
+
export const noObjectParametersRule = defineRule({
|
|
25
|
+
meta: {
|
|
26
|
+
type: "problem",
|
|
27
|
+
docs: {
|
|
28
|
+
description:
|
|
29
|
+
"Disallow object function parameters; inputs must use an owner-provided type and be parsed at their boundary.",
|
|
30
|
+
},
|
|
31
|
+
messages: {
|
|
32
|
+
objectParameter:
|
|
33
|
+
"Parameter `{{parameter}}` uses the broad `object` type. Accept a named owner type; parse external input at its boundary before calling this function.",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
createOnce(context) {
|
|
37
|
+
let environment: TypeAliasEnvironment | null = null;
|
|
38
|
+
|
|
39
|
+
const resolvesToObject = (type: ESTree.TSType): boolean =>
|
|
40
|
+
environment !== null &&
|
|
41
|
+
resolvedTypeMatches(type, environment, (resolved, matches) => {
|
|
42
|
+
if (resolved.type === "TSObjectKeyword") return true;
|
|
43
|
+
if (resolved.type === "TSParenthesizedType") {
|
|
44
|
+
return matches(resolved.typeAnnotation);
|
|
45
|
+
}
|
|
46
|
+
return (
|
|
47
|
+
resolved.type === "TSUnionType" && resolved.types.some(matches)
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const checkParameters = (node: ParameterOwner) => {
|
|
52
|
+
for (const parameter of node.params) {
|
|
53
|
+
const annotation = functionParameterTypeAnnotation(parameter);
|
|
54
|
+
if (annotation === null || annotation === undefined) continue;
|
|
55
|
+
if (!resolvesToObject(annotation.typeAnnotation)) continue;
|
|
56
|
+
context.report({
|
|
57
|
+
node: annotation.typeAnnotation,
|
|
58
|
+
messageId: "objectParameter",
|
|
59
|
+
data: { parameter: functionParameterBindingName(parameter, context.sourceCode) },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
Program(node) {
|
|
66
|
+
environment = createTypeAliasEnvironment(
|
|
67
|
+
node,
|
|
68
|
+
context.sourceCode.visitorKeys,
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
ArrowFunctionExpression: checkParameters,
|
|
72
|
+
FunctionDeclaration: checkParameters,
|
|
73
|
+
FunctionExpression: checkParameters,
|
|
74
|
+
TSCallSignatureDeclaration: checkParameters,
|
|
75
|
+
TSConstructSignatureDeclaration: checkParameters,
|
|
76
|
+
TSConstructorType: checkParameters,
|
|
77
|
+
TSDeclareFunction: checkParameters,
|
|
78
|
+
TSEmptyBodyFunctionExpression: checkParameters,
|
|
79
|
+
TSFunctionType: checkParameters,
|
|
80
|
+
TSMethodSignature: checkParameters,
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import type { ESTree, SourceCode, Variable } from "@oxlint/plugins";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
arrayMethodTarget,
|
|
6
|
+
isKnownArrayExpression,
|
|
7
|
+
resolveArrayBinding,
|
|
8
|
+
unwrapArrayExpression,
|
|
9
|
+
} from "../shared/array-method.ts";
|
|
10
|
+
|
|
11
|
+
function enclosingReducer(node: ESTree.Node) {
|
|
12
|
+
let parent = node.parent;
|
|
13
|
+
while (parent !== null) {
|
|
14
|
+
if (parent.type === "FunctionDeclaration") return null;
|
|
15
|
+
if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") {
|
|
16
|
+
const callback = parent;
|
|
17
|
+
let owner: ESTree.Node | null = callback.parent;
|
|
18
|
+
while (owner !== null && unwrapArrayExpression(owner) === callback) owner = owner.parent;
|
|
19
|
+
if (owner?.type !== "CallExpression") return null;
|
|
20
|
+
const method = arrayMethodTarget(owner.callee);
|
|
21
|
+
const firstArgument = owner.arguments[0];
|
|
22
|
+
if (
|
|
23
|
+
method === null || (method.name !== "reduce" && method.name !== "reduceRight") ||
|
|
24
|
+
owner.arguments.length > 2 || firstArgument === undefined ||
|
|
25
|
+
unwrapArrayExpression(firstArgument) !== callback
|
|
26
|
+
) return null;
|
|
27
|
+
const firstParameter = callback.params[0];
|
|
28
|
+
const accumulator = firstParameter?.type === "AssignmentPattern" ? firstParameter.left : firstParameter;
|
|
29
|
+
if (accumulator?.type !== "Identifier") return null;
|
|
30
|
+
return { callback, accumulator, initialValue: owner.arguments[1] };
|
|
31
|
+
}
|
|
32
|
+
parent = parent.parent;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function referencesAccumulator(
|
|
38
|
+
sourceCode: SourceCode,
|
|
39
|
+
node: ESTree.Node,
|
|
40
|
+
accumulator: Variable,
|
|
41
|
+
visited = new Set<Variable>(),
|
|
42
|
+
): boolean {
|
|
43
|
+
const variable = resolveArrayBinding(sourceCode, node);
|
|
44
|
+
if (variable === null || visited.has(variable)) return false;
|
|
45
|
+
if (variable === accumulator) return true;
|
|
46
|
+
visited.add(variable);
|
|
47
|
+
if (variable.references.some(reference => reference.isWrite() && !reference.init)) return false;
|
|
48
|
+
for (const definition of variable.defs) {
|
|
49
|
+
if (
|
|
50
|
+
definition.type === "Variable" && definition.node.type === "VariableDeclarator" &&
|
|
51
|
+
definition.node.id.type === "Identifier" && definition.node.init !== null &&
|
|
52
|
+
definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const"
|
|
53
|
+
) {
|
|
54
|
+
return referencesAccumulator(sourceCode, definition.node.init, accumulator, visited);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isGlobalCopyOwner(sourceCode: SourceCode, node: ESTree.Node, name: string): boolean {
|
|
61
|
+
node = unwrapArrayExpression(node);
|
|
62
|
+
if (node.type !== "Identifier" || node.name !== name) return false;
|
|
63
|
+
const variable = resolveArrayBinding(sourceCode, node);
|
|
64
|
+
return variable === null || variable.defs.length === 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Reject non-spread copies of reducer accumulators; pair with oxc/no-accumulating-spread. */
|
|
68
|
+
export const noReduceAccumulatorCopyRule = defineRule({
|
|
69
|
+
meta: {
|
|
70
|
+
type: "problem",
|
|
71
|
+
docs: { description: "Disallow copying growing reducer accumulators with Object.assign, Array.from, or array copy methods." },
|
|
72
|
+
messages: {
|
|
73
|
+
accumulatorCopy: "Do not copy the reducer accumulator on every iteration; growing copies can cause quadratic work. Mutate a fresh, locally owned accumulator and return it, or use an iterator pipeline/flatMap.",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
createOnce(context) {
|
|
77
|
+
return {
|
|
78
|
+
CallExpression(node) {
|
|
79
|
+
const method = arrayMethodTarget(node.callee);
|
|
80
|
+
if (method === null) return;
|
|
81
|
+
const reducer = enclosingReducer(node);
|
|
82
|
+
if (reducer === null) return;
|
|
83
|
+
const accumulator = context.sourceCode.getDeclaredVariables(reducer.callback).find(variable =>
|
|
84
|
+
variable.identifiers.some(identifier => identifier.start === reducer.accumulator.start),
|
|
85
|
+
);
|
|
86
|
+
if (accumulator === undefined) return;
|
|
87
|
+
const isAccumulator = (expression: ESTree.Node) =>
|
|
88
|
+
referencesAccumulator(context.sourceCode, expression, accumulator);
|
|
89
|
+
let copiesAccumulator = false;
|
|
90
|
+
if (method.name === "assign" && isGlobalCopyOwner(context.sourceCode, method.object, "Object")) {
|
|
91
|
+
const target = node.arguments[0];
|
|
92
|
+
copiesAccumulator = (
|
|
93
|
+
target !== undefined && unwrapArrayExpression(target).type === "ObjectExpression" &&
|
|
94
|
+
node.arguments.slice(1).some(isAccumulator)
|
|
95
|
+
);
|
|
96
|
+
} else if (method.name === "from" && isGlobalCopyOwner(context.sourceCode, method.object, "Array")) {
|
|
97
|
+
const source = node.arguments[0];
|
|
98
|
+
copiesAccumulator = source !== undefined && isAccumulator(source);
|
|
99
|
+
} else if (["concat", "slice", "toSpliced", "toSorted", "toReversed", "with"].includes(method.name)) {
|
|
100
|
+
const initialValue = reducer.initialValue;
|
|
101
|
+
const arrayAccumulator = initialValue !== undefined &&
|
|
102
|
+
isKnownArrayExpression(context.sourceCode, initialValue);
|
|
103
|
+
copiesAccumulator = arrayAccumulator && isAccumulator(method.object);
|
|
104
|
+
}
|
|
105
|
+
if (copiesAccumulator) context.report({ node, messageId: "accumulatorCopy" });
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
});
|
|
@@ -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
|
+
createOnce(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
|
+
createOnce(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,77 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
4
|
+
|
|
5
|
+
type RuntimeFunction = ESTree.ArrowFunctionExpression | ESTree.Function;
|
|
6
|
+
|
|
7
|
+
function isRuntimeFunction(node: ESTree.Node): node is RuntimeFunction {
|
|
8
|
+
return (
|
|
9
|
+
node.type === "ArrowFunctionExpression" ||
|
|
10
|
+
node.type === "FunctionDeclaration" ||
|
|
11
|
+
node.type === "FunctionExpression"
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isInsideTypeGuard(node: ESTree.Node): boolean {
|
|
16
|
+
let current: ESTree.Node | null = node.parent;
|
|
17
|
+
while (current !== null && current.type !== "Program") {
|
|
18
|
+
if (isRuntimeFunction(current)) {
|
|
19
|
+
return current.returnType?.typeAnnotation.type === "TSTypePredicate";
|
|
20
|
+
}
|
|
21
|
+
current = current.parent;
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Return whether typeof safely probes for the existence of a possibly absent binding. */
|
|
27
|
+
function isExistenceProbe(node: ESTree.UnaryExpression): boolean {
|
|
28
|
+
const parent = node.parent;
|
|
29
|
+
if (parent.type !== "BinaryExpression") return false;
|
|
30
|
+
if (!["===", "!==", "==", "!="].includes(parent.operator)) return false;
|
|
31
|
+
const other = parent.left === node ? parent.right : parent.left;
|
|
32
|
+
return other.type === "Literal" && other.value === "undefined";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Disallow runtime typeof checks that narrow unparsed values instead of decoding them. */
|
|
36
|
+
export const noRuntimeTypeofRule = defineRule({
|
|
37
|
+
meta: {
|
|
38
|
+
type: "problem",
|
|
39
|
+
docs: {
|
|
40
|
+
description:
|
|
41
|
+
"Disallow runtime typeof checks; external values must be decoded into meaningful types at their I/O boundary.",
|
|
42
|
+
},
|
|
43
|
+
messages: {
|
|
44
|
+
runtimeTypeof:
|
|
45
|
+
"A `typeof` check narrows a representation without establishing its contract. Parse input at its I/O boundary, then branch on the domain value.",
|
|
46
|
+
},
|
|
47
|
+
schema: [
|
|
48
|
+
{
|
|
49
|
+
type: "object",
|
|
50
|
+
properties: {
|
|
51
|
+
allowInTypeGuards: { type: "boolean" },
|
|
52
|
+
},
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
defaultOptions: [{ allowInTypeGuards: false }],
|
|
57
|
+
},
|
|
58
|
+
createOnce(context) {
|
|
59
|
+
return {
|
|
60
|
+
UnaryExpression(node) {
|
|
61
|
+
const option = context.options?.[0];
|
|
62
|
+
const allowInTypeGuards =
|
|
63
|
+
typeof option === "object" &&
|
|
64
|
+
option !== null &&
|
|
65
|
+
!Array.isArray(option) &&
|
|
66
|
+
option.allowInTypeGuards === true;
|
|
67
|
+
if (
|
|
68
|
+
node.operator === "typeof" &&
|
|
69
|
+
!isExistenceProbe(node) &&
|
|
70
|
+
(!allowInTypeGuards || !isInsideTypeGuard(node))
|
|
71
|
+
) {
|
|
72
|
+
context.report({ node, messageId: "runtimeTypeof" });
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
3
|
+
|
|
4
|
+
const FORBIDDEN_SYMBOL_NAME = "shape";
|
|
5
|
+
|
|
6
|
+
function containsForbiddenSymbolName(name: string): boolean {
|
|
7
|
+
return name.toLowerCase().includes(FORBIDDEN_SYMBOL_NAME);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Return whether an identifier names a statically accessed member owned by another value. */
|
|
11
|
+
function isBorrowedMemberName(node: ESTree.Node): boolean {
|
|
12
|
+
const parent = node.parent;
|
|
13
|
+
if (parent === null || parent.type !== "MemberExpression") return false;
|
|
14
|
+
return parent.property === node && parent.computed === false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Ban the case-insensitive substring "shape" in every JavaScript and TypeScript symbol name. */
|
|
18
|
+
export const noForbiddenTermInSymbolNamesRule = defineRule({
|
|
19
|
+
meta: {
|
|
20
|
+
type: "problem",
|
|
21
|
+
docs: {
|
|
22
|
+
description:
|
|
23
|
+
'Disallow the case-insensitive substring "shape" in JavaScript, TypeScript, private, and JSX symbol names.',
|
|
24
|
+
},
|
|
25
|
+
messages: {
|
|
26
|
+
forbiddenSymbolName:
|
|
27
|
+
'Rename symbol "{{name}}" for its domain role; "shape" describes structure rather than ownership.',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
createOnce(context) {
|
|
31
|
+
const reportForbiddenSymbolName = (node: ESTree.Node & { name: string }) => {
|
|
32
|
+
if (!containsForbiddenSymbolName(node.name) || isBorrowedMemberName(node)) return;
|
|
33
|
+
context.report({
|
|
34
|
+
node,
|
|
35
|
+
messageId: "forbiddenSymbolName",
|
|
36
|
+
data: { name: node.name },
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
Identifier: reportForbiddenSymbolName,
|
|
42
|
+
PrivateIdentifier: reportForbiddenSymbolName,
|
|
43
|
+
JSXIdentifier: reportForbiddenSymbolName,
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
containsUnknownType,
|
|
6
|
+
functionParameterBindingName,
|
|
7
|
+
functionParameterTypeAnnotation,
|
|
8
|
+
} from "../shared/function-parameters.ts";
|
|
9
|
+
type ParameterOwner =
|
|
10
|
+
| ESTree.ArrowFunctionExpression
|
|
11
|
+
| ESTree.Function
|
|
12
|
+
| ESTree.TSCallSignatureDeclaration
|
|
13
|
+
| ESTree.TSConstructSignatureDeclaration
|
|
14
|
+
| ESTree.TSConstructorType
|
|
15
|
+
| ESTree.TSFunctionType
|
|
16
|
+
| ESTree.TSMethodSignature;
|
|
17
|
+
|
|
18
|
+
function isTypePredicateSubject(owner: ParameterOwner, parameterName: string): boolean {
|
|
19
|
+
const predicate = owner.returnType?.typeAnnotation;
|
|
20
|
+
return (
|
|
21
|
+
predicate?.type === "TSTypePredicate" &&
|
|
22
|
+
predicate.parameterName.type === "Identifier" &&
|
|
23
|
+
predicate.parameterName.name === parameterName
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Disallow unknown inputs except explicitly named error-cause enrichment. */
|
|
28
|
+
export const noUnknownParametersRule = defineRule({
|
|
29
|
+
meta: {
|
|
30
|
+
type: "problem",
|
|
31
|
+
docs: {
|
|
32
|
+
description:
|
|
33
|
+
"Disallow explicitly unknown function parameters except `cause` and type-predicate subjects; decode unknown input at its I/O boundary instead.",
|
|
34
|
+
},
|
|
35
|
+
messages: {
|
|
36
|
+
unknownParameter:
|
|
37
|
+
"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.",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
createOnce(context) {
|
|
41
|
+
const checkParameters = (node: ParameterOwner) => {
|
|
42
|
+
for (const parameter of node.params) {
|
|
43
|
+
const annotation = functionParameterTypeAnnotation(parameter);
|
|
44
|
+
if (annotation === null || annotation === undefined) continue;
|
|
45
|
+
if (!containsUnknownType(annotation.typeAnnotation)) continue;
|
|
46
|
+
const name = functionParameterBindingName(parameter, context.sourceCode);
|
|
47
|
+
if (name === "cause" || isTypePredicateSubject(node, name)) continue;
|
|
48
|
+
context.report({
|
|
49
|
+
node: annotation.typeAnnotation,
|
|
50
|
+
messageId: "unknownParameter",
|
|
51
|
+
data: { parameter: name },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
ArrowFunctionExpression: checkParameters,
|
|
58
|
+
FunctionDeclaration: checkParameters,
|
|
59
|
+
FunctionExpression: checkParameters,
|
|
60
|
+
TSCallSignatureDeclaration: checkParameters,
|
|
61
|
+
TSConstructSignatureDeclaration: checkParameters,
|
|
62
|
+
TSConstructorType: checkParameters,
|
|
63
|
+
TSDeclareFunction: checkParameters,
|
|
64
|
+
TSEmptyBodyFunctionExpression: checkParameters,
|
|
65
|
+
TSFunctionType: checkParameters,
|
|
66
|
+
TSMethodSignature: checkParameters,
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createTypeAliasEnvironment,
|
|
7
|
+
resolvedTypeMatches,
|
|
8
|
+
type TypeAliasEnvironment,
|
|
9
|
+
} from "../shared/type-alias-resolution.ts";
|
|
10
|
+
|
|
11
|
+
type FunctionWithReturnType =
|
|
12
|
+
| ESTree.ArrowFunctionExpression
|
|
13
|
+
| ESTree.Function
|
|
14
|
+
| ESTree.TSCallSignatureDeclaration
|
|
15
|
+
| ESTree.TSConstructSignatureDeclaration
|
|
16
|
+
| ESTree.TSConstructorType
|
|
17
|
+
| ESTree.TSFunctionType
|
|
18
|
+
| ESTree.TSMethodSignature;
|
|
19
|
+
|
|
20
|
+
/** Ban function contracts that return unknown instead of a parsed domain type. */
|
|
21
|
+
export const noUnknownReturnsRule = defineRule({
|
|
22
|
+
meta: {
|
|
23
|
+
type: "problem",
|
|
24
|
+
docs: {
|
|
25
|
+
description:
|
|
26
|
+
"Disallow functions whose explicit return contract is unknown or Promise<unknown>.",
|
|
27
|
+
},
|
|
28
|
+
messages: {
|
|
29
|
+
unknownReturn:
|
|
30
|
+
"This function exposes `unknown` to its caller. Parse the value at its boundary and return a named domain type.",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
createOnce(context) {
|
|
34
|
+
let environment: TypeAliasEnvironment | null = null;
|
|
35
|
+
|
|
36
|
+
const resolvesToUnknown = (type: ESTree.TSType): boolean =>
|
|
37
|
+
environment !== null &&
|
|
38
|
+
resolvedTypeMatches(type, environment, (resolved, matches) => {
|
|
39
|
+
if (resolved.type === "TSUnknownKeyword") return true;
|
|
40
|
+
if (resolved.type === "TSParenthesizedType") {
|
|
41
|
+
return matches(resolved.typeAnnotation);
|
|
42
|
+
}
|
|
43
|
+
if (resolved.type === "TSUnionType") return resolved.types.some(matches);
|
|
44
|
+
if (
|
|
45
|
+
resolved.type !== "TSTypeReference" ||
|
|
46
|
+
resolved.typeName.type !== "Identifier" ||
|
|
47
|
+
(resolved.typeName.name !== "Promise" &&
|
|
48
|
+
resolved.typeName.name !== "PromiseLike")
|
|
49
|
+
) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const value = resolved.typeArguments?.params[0];
|
|
53
|
+
return value !== undefined && matches(value);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const checkReturnType = (node: FunctionWithReturnType) => {
|
|
57
|
+
const annotation = node.returnType;
|
|
58
|
+
if (annotation === null || annotation === undefined) return;
|
|
59
|
+
if (!resolvesToUnknown(annotation.typeAnnotation)) return;
|
|
60
|
+
context.report({ node: annotation.typeAnnotation, messageId: "unknownReturn" });
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
Program(node) {
|
|
65
|
+
environment = createTypeAliasEnvironment(
|
|
66
|
+
node,
|
|
67
|
+
context.sourceCode.visitorKeys,
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
ArrowFunctionExpression: checkReturnType,
|
|
71
|
+
FunctionDeclaration: checkReturnType,
|
|
72
|
+
FunctionExpression: checkReturnType,
|
|
73
|
+
TSCallSignatureDeclaration: checkReturnType,
|
|
74
|
+
TSConstructSignatureDeclaration: checkReturnType,
|
|
75
|
+
TSConstructorType: checkReturnType,
|
|
76
|
+
TSDeclareFunction: checkReturnType,
|
|
77
|
+
TSEmptyBodyFunctionExpression: checkReturnType,
|
|
78
|
+
TSFunctionType: checkReturnType,
|
|
79
|
+
TSMethodSignature: checkReturnType,
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createTypeAliasEnvironment,
|
|
7
|
+
resolvedTypeMatches,
|
|
8
|
+
type TypeAliasEnvironment,
|
|
9
|
+
} from "../shared/type-alias-resolution.ts";
|
|
10
|
+
|
|
11
|
+
/** Ban named aliases that merely conceal TypeScript's unknown top type. */
|
|
12
|
+
export const noUnknownTypeAliasesRule = defineRule({
|
|
13
|
+
meta: {
|
|
14
|
+
type: "problem",
|
|
15
|
+
docs: {
|
|
16
|
+
description:
|
|
17
|
+
"Disallow type aliases whose resolved type is unknown; unknown must remain visible at an allowed boundary.",
|
|
18
|
+
},
|
|
19
|
+
messages: {
|
|
20
|
+
unknownAlias:
|
|
21
|
+
"Type alias `{{alias}}` hides `unknown`. Keep `unknown` explicit at the parsing boundary or on an allowed `cause` field; otherwise use the parsed owner type.",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
createOnce(context) {
|
|
25
|
+
let environment: TypeAliasEnvironment | null = null;
|
|
26
|
+
|
|
27
|
+
const resolvesToUnknown = (type: ESTree.TSType): boolean =>
|
|
28
|
+
environment !== null &&
|
|
29
|
+
resolvedTypeMatches(type, environment, (resolved, matches) => {
|
|
30
|
+
if (resolved.type === "TSUnknownKeyword") return true;
|
|
31
|
+
if (resolved.type === "TSParenthesizedType") {
|
|
32
|
+
return matches(resolved.typeAnnotation);
|
|
33
|
+
}
|
|
34
|
+
return resolved.type === "TSUnionType" && resolved.types.some(matches);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
Program(node) {
|
|
39
|
+
environment = createTypeAliasEnvironment(
|
|
40
|
+
node,
|
|
41
|
+
context.sourceCode.visitorKeys,
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
TSTypeAliasDeclaration(node) {
|
|
45
|
+
if (!resolvesToUnknown(node.typeAnnotation)) return;
|
|
46
|
+
context.report({
|
|
47
|
+
node: node.id,
|
|
48
|
+
messageId: "unknownAlias",
|
|
49
|
+
data: { alias: node.id.name },
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
});
|