@configg/oxc 0.1.4 → 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 +8 -2
- 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 +28 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
classifyUnsafeDictionary,
|
|
5
|
+
classifyUnsafeDictionaryValue,
|
|
6
|
+
createTypeEnvironment,
|
|
7
|
+
type TypeEnvironment,
|
|
8
|
+
} from "../shared/dictionary-types.ts";
|
|
9
|
+
import { visibleTypeAlias } from "../shared/type-alias-resolution.ts";
|
|
10
|
+
|
|
11
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
12
|
+
|
|
13
|
+
const typeNodeKinds: ReadonlySet<string> = new Set([
|
|
14
|
+
"JSDocNonNullableType",
|
|
15
|
+
"JSDocNullableType",
|
|
16
|
+
"JSDocUnknownType",
|
|
17
|
+
"TSAnyKeyword",
|
|
18
|
+
"TSArrayType",
|
|
19
|
+
"TSBigIntKeyword",
|
|
20
|
+
"TSBooleanKeyword",
|
|
21
|
+
"TSConditionalType",
|
|
22
|
+
"TSConstructorType",
|
|
23
|
+
"TSFunctionType",
|
|
24
|
+
"TSImportType",
|
|
25
|
+
"TSIndexedAccessType",
|
|
26
|
+
"TSInferType",
|
|
27
|
+
"TSIntersectionType",
|
|
28
|
+
"TSIntrinsicKeyword",
|
|
29
|
+
"TSLiteralType",
|
|
30
|
+
"TSMappedType",
|
|
31
|
+
"TSNamedTupleMember",
|
|
32
|
+
"TSNeverKeyword",
|
|
33
|
+
"TSNullKeyword",
|
|
34
|
+
"TSNumberKeyword",
|
|
35
|
+
"TSObjectKeyword",
|
|
36
|
+
"TSParenthesizedType",
|
|
37
|
+
"TSStringKeyword",
|
|
38
|
+
"TSSymbolKeyword",
|
|
39
|
+
"TSTemplateLiteralType",
|
|
40
|
+
"TSThisType",
|
|
41
|
+
"TSTupleType",
|
|
42
|
+
"TSTypeLiteral",
|
|
43
|
+
"TSTypeOperator",
|
|
44
|
+
"TSTypePredicate",
|
|
45
|
+
"TSTypeQuery",
|
|
46
|
+
"TSTypeReference",
|
|
47
|
+
"TSUndefinedKeyword",
|
|
48
|
+
"TSUnionType",
|
|
49
|
+
"TSUnknownKeyword",
|
|
50
|
+
"TSVoidKeyword",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
function isTypeNode(node: ESTree.Node): node is ESTree.TSType {
|
|
54
|
+
return typeNodeKinds.has(node.type);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function typeReferenceName(type: ESTree.TSTypeReference): string | null {
|
|
58
|
+
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isInsideTypeAliasDeclaration(node: ESTree.Node): boolean {
|
|
62
|
+
let current: ESTree.Node | null = node.parent;
|
|
63
|
+
while (current !== null && current.type !== "Program") {
|
|
64
|
+
if (current.type === "TSTypeAliasDeclaration") return true;
|
|
65
|
+
current = current.parent;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isPlainAliasConsumerUse(node: ESTree.TSType, environment: TypeEnvironment): boolean {
|
|
71
|
+
if (node.type !== "TSTypeReference" || node.typeArguments?.params.length) return false;
|
|
72
|
+
const name = typeReferenceName(node);
|
|
73
|
+
return (
|
|
74
|
+
name !== null &&
|
|
75
|
+
visibleTypeAlias(name, node, environment.typeAliases) !== null &&
|
|
76
|
+
!isInsideTypeAliasDeclaration(node)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isInsideTypeParameterConstraint(node: ESTree.TSType): boolean {
|
|
81
|
+
let child: ESTree.Node = node;
|
|
82
|
+
let parent: ESTree.Node | null = child.parent;
|
|
83
|
+
while (parent !== null && parent.type !== "Program") {
|
|
84
|
+
if (parent.type === "TSTypeParameter" && parent.constraint === child) return true;
|
|
85
|
+
child = parent;
|
|
86
|
+
parent = child.parent;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function shouldReportType(node: ESTree.TSType, environment: TypeEnvironment): boolean {
|
|
92
|
+
if (isInsideTypeParameterConstraint(node)) return false;
|
|
93
|
+
if (isPlainAliasConsumerUse(node, environment)) return false;
|
|
94
|
+
if (classifyUnsafeDictionary(node, environment) === null) return false;
|
|
95
|
+
let current: ESTree.Node | null = node.parent;
|
|
96
|
+
while (current !== null && current.type !== "Program") {
|
|
97
|
+
if (isTypeNode(current) && classifyUnsafeDictionary(current, environment) !== null)
|
|
98
|
+
return false;
|
|
99
|
+
current = current.parent;
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Disallow object-dictionary contracts whose direct value type is an unsafe escape hatch. */
|
|
105
|
+
export const noUnsafeDictionaryTypeRule = defineRule({
|
|
106
|
+
meta: {
|
|
107
|
+
type: "problem",
|
|
108
|
+
docs: {
|
|
109
|
+
description:
|
|
110
|
+
"Disallow object-dictionary contracts whose direct value type is unknown, any, object, {}, or a union/alias containing one of those escape hatches.",
|
|
111
|
+
},
|
|
112
|
+
messages: {
|
|
113
|
+
unsafeDictionary:
|
|
114
|
+
"This dictionary's {{value}} value type gives callers no concrete value contract. Use an owner/schema-derived value type; parse external payloads before insertion.",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
createOnce(context) {
|
|
118
|
+
let environment: TypeEnvironment | null = null;
|
|
119
|
+
const report = (node: ESTree.Node, value: string) => {
|
|
120
|
+
context.report({ node, messageId: "unsafeDictionary", data: { value } });
|
|
121
|
+
};
|
|
122
|
+
const reportIfUnsafe = (node: ESTree.TSType) => {
|
|
123
|
+
if (environment === null || !shouldReportType(node, environment)) return;
|
|
124
|
+
const unsafe = classifyUnsafeDictionary(node, environment);
|
|
125
|
+
if (unsafe === null) return;
|
|
126
|
+
report(node, unsafe.unsafeValue);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
Program(node) {
|
|
131
|
+
environment = createTypeEnvironment(
|
|
132
|
+
node,
|
|
133
|
+
context.sourceCode.visitorKeys,
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
TSTypeReference: reportIfUnsafe,
|
|
137
|
+
TSTypeLiteral: reportIfUnsafe,
|
|
138
|
+
TSMappedType: reportIfUnsafe,
|
|
139
|
+
TSIndexSignature(node) {
|
|
140
|
+
if (
|
|
141
|
+
environment === null ||
|
|
142
|
+
node.typeAnnotation === null ||
|
|
143
|
+
node.parent.type === "TSTypeLiteral"
|
|
144
|
+
)
|
|
145
|
+
return;
|
|
146
|
+
const unsafe = classifyUnsafeDictionaryValue(
|
|
147
|
+
node.typeAnnotation.typeAnnotation,
|
|
148
|
+
environment,
|
|
149
|
+
);
|
|
150
|
+
if (unsafe !== null) report(node, unsafe.unsafeValue);
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
});
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import type { ESTree, Variable } from "@oxlint/plugins";
|
|
3
|
+
|
|
4
|
+
type BroadTypeKind = "top" | "object" | "record";
|
|
5
|
+
|
|
6
|
+
type KnownValueEvidence = {
|
|
7
|
+
readonly type: ESTree.TSType | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const functionBoundaryTypes = new Set([
|
|
11
|
+
"ArrowFunctionExpression",
|
|
12
|
+
"FunctionDeclaration",
|
|
13
|
+
"FunctionExpression",
|
|
14
|
+
"TSDeclareFunction",
|
|
15
|
+
"TSEmptyBodyFunctionExpression",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function unwrapExpressionParentheses(expression: ESTree.Expression): ESTree.Expression {
|
|
19
|
+
let current = expression;
|
|
20
|
+
while (current.type === "ParenthesizedExpression") current = current.expression;
|
|
21
|
+
return current;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function unwrapTypeParentheses(type: ESTree.TSType): ESTree.TSType {
|
|
25
|
+
let current = type;
|
|
26
|
+
while (current.type === "TSParenthesizedType") current = current.typeAnnotation;
|
|
27
|
+
return current;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function typeReferenceName(type: ESTree.TSTypeReference): string | null {
|
|
31
|
+
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isUnknownOrAnyType(type: ESTree.TSType): boolean {
|
|
35
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
36
|
+
return unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isBroadRecordKeyType(type: ESTree.TSType): boolean {
|
|
40
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
41
|
+
if (
|
|
42
|
+
unwrapped.type === "TSStringKeyword" ||
|
|
43
|
+
unwrapped.type === "TSNumberKeyword" ||
|
|
44
|
+
unwrapped.type === "TSSymbolKeyword"
|
|
45
|
+
) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
if (unwrapped.type === "TSUnionType") return unwrapped.types.every(isBroadRecordKeyType);
|
|
49
|
+
return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isBroadRecordType(type: ESTree.TSType): boolean {
|
|
53
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
54
|
+
|
|
55
|
+
if (unwrapped.type === "TSTypeReference") {
|
|
56
|
+
if (typeReferenceName(unwrapped) === "Readonly") {
|
|
57
|
+
const [inner] = unwrapped.typeArguments?.params ?? [];
|
|
58
|
+
return inner !== undefined && isBroadRecordType(inner);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (typeReferenceName(unwrapped) !== "Record") return false;
|
|
62
|
+
const parameters = unwrapped.typeArguments?.params ?? [];
|
|
63
|
+
return (
|
|
64
|
+
parameters.length === 2 &&
|
|
65
|
+
parameters[0] !== undefined &&
|
|
66
|
+
parameters[1] !== undefined &&
|
|
67
|
+
isBroadRecordKeyType(parameters[0]) &&
|
|
68
|
+
isUnknownOrAnyType(parameters[1])
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) return false;
|
|
73
|
+
const [member] = unwrapped.members;
|
|
74
|
+
const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : [];
|
|
75
|
+
return (
|
|
76
|
+
member?.type === "TSIndexSignature" &&
|
|
77
|
+
member.parameters.length === 1 &&
|
|
78
|
+
parameter !== undefined &&
|
|
79
|
+
isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) &&
|
|
80
|
+
isUnknownOrAnyType(member.typeAnnotation.typeAnnotation)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function broadTypeKind(type: ESTree.TSType): BroadTypeKind | null {
|
|
85
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
86
|
+
if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") return "top";
|
|
87
|
+
if (unwrapped.type === "TSObjectKeyword") return "object";
|
|
88
|
+
return isBroadRecordType(unwrapped) ? "record" : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function assertedExpression(
|
|
92
|
+
node: ESTree.TSAsExpression | ESTree.TSTypeAssertion,
|
|
93
|
+
): ESTree.Expression {
|
|
94
|
+
return unwrapExpressionParentheses(node.expression);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function assertionFromExpression(
|
|
98
|
+
expression: ESTree.Expression,
|
|
99
|
+
): ESTree.TSAsExpression | ESTree.TSTypeAssertion | null {
|
|
100
|
+
const unwrapped = unwrapExpressionParentheses(expression);
|
|
101
|
+
return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion"
|
|
102
|
+
? unwrapped
|
|
103
|
+
: null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizedTypeText(sourceText: string, type: ESTree.TSType): string {
|
|
107
|
+
return sourceText.slice(type.start, type.end).replaceAll(/\s+/gu, "");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function typesHaveSameSyntax(
|
|
111
|
+
sourceText: string,
|
|
112
|
+
left: ESTree.TSType | null,
|
|
113
|
+
right: ESTree.TSType,
|
|
114
|
+
): boolean {
|
|
115
|
+
return (
|
|
116
|
+
left !== null &&
|
|
117
|
+
normalizedTypeText(sourceText, unwrapTypeParentheses(left)) ===
|
|
118
|
+
normalizedTypeText(sourceText, unwrapTypeParentheses(right))
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isDefinitelyObjectType(type: ESTree.TSType): boolean {
|
|
123
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
124
|
+
switch (unwrapped.type) {
|
|
125
|
+
case "TSArrayType":
|
|
126
|
+
case "TSConstructorType":
|
|
127
|
+
case "TSFunctionType":
|
|
128
|
+
case "TSMappedType":
|
|
129
|
+
case "TSObjectKeyword":
|
|
130
|
+
case "TSTupleType":
|
|
131
|
+
return true;
|
|
132
|
+
case "TSTypeLiteral":
|
|
133
|
+
return unwrapped.members.length > 0;
|
|
134
|
+
case "TSIntersectionType":
|
|
135
|
+
return unwrapped.types.every(isDefinitelyObjectType);
|
|
136
|
+
case "TSTypeOperator":
|
|
137
|
+
return unwrapped.operator === "readonly" && isDefinitelyObjectType(unwrapped.typeAnnotation);
|
|
138
|
+
default:
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isDefinitelyNarrowerRecordType(type: ESTree.TSType): boolean {
|
|
144
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
145
|
+
if (unwrapped.type === "TSTypeLiteral") {
|
|
146
|
+
return unwrapped.members.some((member) => member.type !== "TSIndexSignature");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (unwrapped.type !== "TSTypeReference") return false;
|
|
150
|
+
if (typeReferenceName(unwrapped) === "Readonly") {
|
|
151
|
+
const [inner] = unwrapped.typeArguments?.params ?? [];
|
|
152
|
+
return inner !== undefined && isDefinitelyNarrowerRecordType(inner);
|
|
153
|
+
}
|
|
154
|
+
if (typeReferenceName(unwrapped) !== "Record") return false;
|
|
155
|
+
|
|
156
|
+
const parameters = unwrapped.typeArguments?.params ?? [];
|
|
157
|
+
return (
|
|
158
|
+
parameters.length === 2 && parameters[1] !== undefined && !isUnknownOrAnyType(parameters[1])
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function functionBoundary(node: ESTree.Node): ESTree.Node | null {
|
|
163
|
+
let current = node.parent;
|
|
164
|
+
while (current !== null && current.type !== "Program") {
|
|
165
|
+
if (functionBoundaryTypes.has(current.type)) return current;
|
|
166
|
+
current = current.parent;
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function resolvedVariableForIdentifier(
|
|
172
|
+
scopes: readonly {
|
|
173
|
+
readonly references: readonly {
|
|
174
|
+
readonly identifier: ESTree.Node;
|
|
175
|
+
readonly resolved: Variable | null;
|
|
176
|
+
}[];
|
|
177
|
+
}[],
|
|
178
|
+
identifier: ESTree.IdentifierReference,
|
|
179
|
+
): Variable | null {
|
|
180
|
+
for (const scope of scopes) {
|
|
181
|
+
const reference = scope.references.find(
|
|
182
|
+
(candidate) =>
|
|
183
|
+
candidate.identifier.start === identifier.start &&
|
|
184
|
+
candidate.identifier.end === identifier.end,
|
|
185
|
+
);
|
|
186
|
+
if (reference !== undefined) return reference.resolved;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function variableDeclarator(variable: Variable): ESTree.VariableDeclarator | null {
|
|
192
|
+
for (const definition of variable.defs) {
|
|
193
|
+
if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") {
|
|
194
|
+
return definition.node;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function knownValueEvidence(
|
|
201
|
+
expression: ESTree.Expression,
|
|
202
|
+
scopes: Parameters<typeof resolvedVariableForIdentifier>[0],
|
|
203
|
+
boundary: ESTree.Node | null,
|
|
204
|
+
visitedVariables: ReadonlySet<Variable>,
|
|
205
|
+
): KnownValueEvidence | null {
|
|
206
|
+
const unwrapped = unwrapExpressionParentheses(expression);
|
|
207
|
+
|
|
208
|
+
if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") {
|
|
209
|
+
if (broadTypeKind(unwrapped.typeAnnotation) !== null) return null;
|
|
210
|
+
return { type: unwrapped.typeAnnotation };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") {
|
|
214
|
+
return { type: null };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (
|
|
218
|
+
unwrapped.type === "ArrayExpression" ||
|
|
219
|
+
unwrapped.type === "ArrowFunctionExpression" ||
|
|
220
|
+
unwrapped.type === "ClassExpression" ||
|
|
221
|
+
unwrapped.type === "FunctionExpression" ||
|
|
222
|
+
unwrapped.type === "NewExpression" ||
|
|
223
|
+
unwrapped.type === "ObjectExpression"
|
|
224
|
+
) {
|
|
225
|
+
return { type: null };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (unwrapped.type !== "Identifier") return null;
|
|
229
|
+
const variable = resolvedVariableForIdentifier(scopes, unwrapped);
|
|
230
|
+
if (variable === null || visitedVariables.has(variable)) return null;
|
|
231
|
+
|
|
232
|
+
const annotatedIdentifier = variable.identifiers.find(
|
|
233
|
+
(identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== undefined,
|
|
234
|
+
);
|
|
235
|
+
const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation;
|
|
236
|
+
if (annotation !== undefined && annotatedIdentifier !== undefined) {
|
|
237
|
+
if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
return { type: annotation };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const declarator = variableDeclarator(variable);
|
|
244
|
+
if (
|
|
245
|
+
declarator === null ||
|
|
246
|
+
declarator.parent.type !== "VariableDeclaration" ||
|
|
247
|
+
declarator.parent.kind !== "const" ||
|
|
248
|
+
declarator.init === null ||
|
|
249
|
+
variable.references.some((reference) => reference.isWrite() && !reference.init) ||
|
|
250
|
+
functionBoundary(declarator) !== boundary
|
|
251
|
+
) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return knownValueEvidence(
|
|
256
|
+
declarator.init,
|
|
257
|
+
scopes,
|
|
258
|
+
boundary,
|
|
259
|
+
new Set([...visitedVariables, variable]),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function widenedBinding(
|
|
264
|
+
variable: Variable,
|
|
265
|
+
scopes: Parameters<typeof resolvedVariableForIdentifier>[0],
|
|
266
|
+
): {
|
|
267
|
+
readonly broadKind: BroadTypeKind;
|
|
268
|
+
readonly evidence: KnownValueEvidence;
|
|
269
|
+
readonly declaredAt: number;
|
|
270
|
+
readonly boundary: ESTree.Node | null;
|
|
271
|
+
} | null {
|
|
272
|
+
const declarator = variableDeclarator(variable);
|
|
273
|
+
if (
|
|
274
|
+
declarator === null ||
|
|
275
|
+
declarator.parent.type !== "VariableDeclaration" ||
|
|
276
|
+
declarator.parent.kind !== "const" ||
|
|
277
|
+
declarator.id.type !== "Identifier" ||
|
|
278
|
+
declarator.init === null ||
|
|
279
|
+
variable.references.some((reference) => reference.isWrite() && !reference.init)
|
|
280
|
+
) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const boundary = functionBoundary(declarator);
|
|
285
|
+
const declaredType = declarator.id.typeAnnotation?.typeAnnotation;
|
|
286
|
+
const initializerAssertion = assertionFromExpression(declarator.init);
|
|
287
|
+
const initializerBroadKind =
|
|
288
|
+
initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation);
|
|
289
|
+
const declaredBroadKind = declaredType === undefined ? null : broadTypeKind(declaredType);
|
|
290
|
+
const broadKind = declaredBroadKind ?? initializerBroadKind;
|
|
291
|
+
if (broadKind === null) return null;
|
|
292
|
+
|
|
293
|
+
const originalExpression =
|
|
294
|
+
initializerAssertion !== null && initializerBroadKind !== null
|
|
295
|
+
? assertedExpression(initializerAssertion)
|
|
296
|
+
: declarator.init;
|
|
297
|
+
const evidence = knownValueEvidence(originalExpression, scopes, boundary, new Set([variable]));
|
|
298
|
+
return evidence === null ? null : { broadKind, evidence, declaredAt: declarator.end, boundary };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function assertionIsNarrower(
|
|
302
|
+
sourceText: string,
|
|
303
|
+
broadKind: BroadTypeKind,
|
|
304
|
+
evidence: KnownValueEvidence,
|
|
305
|
+
assertedType: ESTree.TSType,
|
|
306
|
+
): boolean {
|
|
307
|
+
if (broadTypeKind(assertedType) !== null) return false;
|
|
308
|
+
if (broadKind === "top") return true;
|
|
309
|
+
if (typesHaveSameSyntax(sourceText, evidence.type, assertedType)) return true;
|
|
310
|
+
if (broadKind === "object") return isDefinitelyObjectType(assertedType);
|
|
311
|
+
return isDefinitelyNarrowerRecordType(assertedType);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Detect immutable local bindings that erase a known type and are later asserted back to a narrower type. */
|
|
315
|
+
export const noWidenThenAssertRule = defineRule({
|
|
316
|
+
meta: {
|
|
317
|
+
type: "problem",
|
|
318
|
+
docs: {
|
|
319
|
+
description:
|
|
320
|
+
"Disallow local const flows that explicitly widen a known value before asserting the widened binding to a narrower type.",
|
|
321
|
+
},
|
|
322
|
+
messages: {
|
|
323
|
+
widenThenAssert:
|
|
324
|
+
'Binding "{{name}}" discards type evidence and later recreates it with an assertion. Keep the precise type from initialization through use; parse boundary input once.',
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
createOnce(context) {
|
|
328
|
+
let scopes: Parameters<typeof resolvedVariableForIdentifier>[0] = [];
|
|
329
|
+
|
|
330
|
+
const checkAssertion = (node: ESTree.TSAsExpression | ESTree.TSTypeAssertion) => {
|
|
331
|
+
const expression = assertedExpression(node);
|
|
332
|
+
if (expression.type !== "Identifier") return;
|
|
333
|
+
|
|
334
|
+
const variable = resolvedVariableForIdentifier(scopes, expression);
|
|
335
|
+
if (variable === null) return;
|
|
336
|
+
const widened = widenedBinding(variable, scopes);
|
|
337
|
+
if (
|
|
338
|
+
widened === null ||
|
|
339
|
+
node.start <= widened.declaredAt ||
|
|
340
|
+
functionBoundary(node) !== widened.boundary ||
|
|
341
|
+
!assertionIsNarrower(
|
|
342
|
+
context.sourceCode.text,
|
|
343
|
+
widened.broadKind,
|
|
344
|
+
widened.evidence,
|
|
345
|
+
node.typeAnnotation,
|
|
346
|
+
)
|
|
347
|
+
) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
context.report({
|
|
352
|
+
node,
|
|
353
|
+
messageId: "widenThenAssert",
|
|
354
|
+
data: { name: expression.name },
|
|
355
|
+
});
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
Program() {
|
|
360
|
+
scopes = context.sourceCode.scopeManager.scopes;
|
|
361
|
+
},
|
|
362
|
+
TSAsExpression: checkAssertion,
|
|
363
|
+
TSTypeAssertion: checkAssertion,
|
|
364
|
+
};
|
|
365
|
+
},
|
|
366
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CreateRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import createPaddingLineRule from "../vendor/eslint-stylistic/padding-line-between-statements.ts";
|
|
4
|
+
|
|
5
|
+
const paddingRule = createPaddingLineRule([
|
|
6
|
+
{ blankLine: "always", prev: "import", next: "*" },
|
|
7
|
+
{ blankLine: "always", prev: "*", next: { selector: "Program > :not(ImportDeclaration)" } },
|
|
8
|
+
{ blankLine: "always", prev: { selector: "Program > :not(ImportDeclaration)" }, next: "*" },
|
|
9
|
+
{ blankLine: "always", prev: "*", next: ["function", "class", "interface", "type"] },
|
|
10
|
+
{ blankLine: "always", prev: ["function", "class", "interface", "type"], next: "*" },
|
|
11
|
+
{
|
|
12
|
+
blankLine: "always",
|
|
13
|
+
prev: "*",
|
|
14
|
+
next: ["multiline-const", "multiline-let", "multiline-var", "multiline-using"],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
blankLine: "always",
|
|
18
|
+
prev: ["multiline-const", "multiline-let", "multiline-var", "multiline-using"],
|
|
19
|
+
next: "*",
|
|
20
|
+
},
|
|
21
|
+
{ blankLine: "always", prev: "*", next: ["return", "if", "switch", "try", "for", "while", "do"] },
|
|
22
|
+
{ blankLine: "always", prev: "block-like", next: "*" },
|
|
23
|
+
{ blankLine: "any", prev: "import", next: "import" },
|
|
24
|
+
{
|
|
25
|
+
blankLine: "any",
|
|
26
|
+
prev: {
|
|
27
|
+
selector:
|
|
28
|
+
':matches(TSDeclareFunction, ExportNamedDeclaration[declaration.type="TSDeclareFunction"])',
|
|
29
|
+
},
|
|
30
|
+
next: {
|
|
31
|
+
selector:
|
|
32
|
+
':matches(TSDeclareFunction, FunctionDeclaration, ExportNamedDeclaration[declaration.type="TSDeclareFunction"], ExportNamedDeclaration[declaration.type="FunctionDeclaration"])',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/** Restore structural blank lines with whitespace-only fixes; keep local short bindings and overloads grouped. */
|
|
38
|
+
export const requireReadableSpacingRule: CreateRule = {
|
|
39
|
+
...paddingRule,
|
|
40
|
+
meta: {
|
|
41
|
+
...paddingRule.meta,
|
|
42
|
+
docs: {
|
|
43
|
+
description: "Require readable spacing between declarations and logical statement groups.",
|
|
44
|
+
},
|
|
45
|
+
schema: [],
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import type { ESTree, SourceCode } from "@oxlint/plugins";
|
|
4
|
+
|
|
5
|
+
type TypeAssertion = ESTree.TSAsExpression | ESTree.TSTypeAssertion;
|
|
6
|
+
|
|
7
|
+
const DEFAULT_SAFETY_MARKERS = ["SAFETY"] as const;
|
|
8
|
+
|
|
9
|
+
const commentOwnerKinds = new Set([
|
|
10
|
+
"ExpressionStatement",
|
|
11
|
+
"PropertyDefinition",
|
|
12
|
+
"ReturnStatement",
|
|
13
|
+
"ThrowStatement",
|
|
14
|
+
"VariableDeclaration",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function isConstAssertion(node: TypeAssertion): boolean {
|
|
18
|
+
return (
|
|
19
|
+
node.typeAnnotation.type === "TSTypeReference" &&
|
|
20
|
+
node.typeAnnotation.typeName.type === "Identifier" &&
|
|
21
|
+
node.typeAnnotation.typeName.name === "const"
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function configuredSafetyMarkers(option: unknown): readonly string[] {
|
|
26
|
+
if (typeof option !== "object" || option === null || !("markers" in option)) {
|
|
27
|
+
return DEFAULT_SAFETY_MARKERS;
|
|
28
|
+
}
|
|
29
|
+
const configured = option.markers;
|
|
30
|
+
if (!Array.isArray(configured)) return DEFAULT_SAFETY_MARKERS;
|
|
31
|
+
const markers = configured.flatMap((marker) =>
|
|
32
|
+
typeof marker === "string" && marker.trim().length > 0 ? [marker.trim()] : [],
|
|
33
|
+
);
|
|
34
|
+
return markers.length > 0 ? markers : DEFAULT_SAFETY_MARKERS;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function markerPattern(markers: readonly string[]): RegExp {
|
|
38
|
+
const alternation = markers
|
|
39
|
+
.map((marker) => marker.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`))
|
|
40
|
+
.join("|");
|
|
41
|
+
return new RegExp(
|
|
42
|
+
String.raw`(?:^|[^\p{L}\p{N}_])(?:${alternation})\s*:\s*\S`,
|
|
43
|
+
"u",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasSafetyJustificationBefore(
|
|
48
|
+
sourceCode: SourceCode,
|
|
49
|
+
owner: ESTree.Node,
|
|
50
|
+
assertion: TypeAssertion,
|
|
51
|
+
pattern: RegExp,
|
|
52
|
+
): boolean {
|
|
53
|
+
return sourceCode
|
|
54
|
+
.getCommentsBefore(owner)
|
|
55
|
+
.some(
|
|
56
|
+
(comment) => comment.end <= assertion.start && pattern.test(comment.value),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function hasSafetyComment(
|
|
61
|
+
sourceCode: SourceCode,
|
|
62
|
+
node: TypeAssertion,
|
|
63
|
+
pattern: RegExp,
|
|
64
|
+
): boolean {
|
|
65
|
+
let current: ESTree.Node = node;
|
|
66
|
+
while (true) {
|
|
67
|
+
if (hasSafetyJustificationBefore(sourceCode, current, node, pattern)) return true;
|
|
68
|
+
if (commentOwnerKinds.has(current.type)) {
|
|
69
|
+
const exportDeclaration = current.parent;
|
|
70
|
+
return (
|
|
71
|
+
exportDeclaration.type === "ExportNamedDeclaration" &&
|
|
72
|
+
exportDeclaration.declaration === current &&
|
|
73
|
+
hasSafetyJustificationBefore(sourceCode, exportDeclaration, node, pattern)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (current.parent.type === "Program") return false;
|
|
77
|
+
current = current.parent;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Require every non-const type assertion to state the invariant TypeScript cannot express. */
|
|
82
|
+
export const requireSafetyCommentForTypeAssertionRule = defineRule({
|
|
83
|
+
meta: {
|
|
84
|
+
type: "problem",
|
|
85
|
+
docs: {
|
|
86
|
+
description:
|
|
87
|
+
"Require a nearby SAFETY comment for every TypeScript type assertion except const assertions.",
|
|
88
|
+
},
|
|
89
|
+
messages: {
|
|
90
|
+
missingSafetyComment:
|
|
91
|
+
"This type assertion has no `{{marker}}:` justification. State the checked invariant immediately before the assertion or its containing statement.",
|
|
92
|
+
},
|
|
93
|
+
schema: [
|
|
94
|
+
{
|
|
95
|
+
type: "object",
|
|
96
|
+
properties: {
|
|
97
|
+
markers: {
|
|
98
|
+
type: "array",
|
|
99
|
+
items: { type: "string", minLength: 1 },
|
|
100
|
+
minItems: 1,
|
|
101
|
+
uniqueItems: true,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
additionalProperties: false,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
defaultOptions: [{ markers: ["SAFETY"] }],
|
|
108
|
+
},
|
|
109
|
+
createOnce(context) {
|
|
110
|
+
const patterns = new Map<string, RegExp>();
|
|
111
|
+
|
|
112
|
+
const checkAssertion = (node: TypeAssertion) => {
|
|
113
|
+
if (isConstAssertion(node)) return;
|
|
114
|
+
const markers = configuredSafetyMarkers(context.options?.[0]);
|
|
115
|
+
const patternKey = markers.join("\u0000");
|
|
116
|
+
const pattern = patterns.get(patternKey) ?? markerPattern(markers);
|
|
117
|
+
patterns.set(patternKey, pattern);
|
|
118
|
+
if (hasSafetyComment(context.sourceCode, node, pattern)) return;
|
|
119
|
+
context.report({
|
|
120
|
+
node,
|
|
121
|
+
messageId: "missingSafetyComment",
|
|
122
|
+
data: { marker: markers[0] ?? DEFAULT_SAFETY_MARKERS[0] },
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
TSAsExpression: checkAssertion,
|
|
128
|
+
TSTypeAssertion: checkAssertion,
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
});
|