ai-deslop 0.2.0 → 0.2.2
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 +35 -106
- package/dist/index.d.ts +18 -0
- package/dist/index.js +84 -0
- package/dist/rules/no-array-filter-map.d.ts +2 -0
- package/dist/rules/no-array-filter-map.js +30 -0
- package/dist/rules/no-call-only-assertions.d.ts +2 -0
- package/dist/rules/no-call-only-assertions.js +87 -0
- package/dist/rules/no-chained-type-assertions.d.ts +2 -0
- package/dist/rules/no-chained-type-assertions.js +60 -0
- package/dist/rules/no-conditional-empty-object-spread.d.ts +2 -0
- package/dist/rules/no-conditional-empty-object-spread.js +40 -0
- package/dist/rules/no-duplicate-types.d.ts +2 -0
- package/dist/rules/no-duplicate-types.js +143 -0
- package/dist/rules/no-known-value-widening.d.ts +2 -0
- package/dist/rules/no-known-value-widening.js +300 -0
- package/dist/rules/no-module-mocking.d.ts +2 -0
- package/dist/rules/no-module-mocking.js +69 -0
- package/dist/rules/no-object-parameters.d.ts +2 -0
- package/dist/rules/no-object-parameters.js +56 -0
- package/dist/rules/no-pass-through-type-alias.d.ts +2 -0
- package/dist/rules/no-pass-through-type-alias.js +41 -0
- package/dist/rules/no-reduce-accumulator-copy.d.ts +2 -0
- package/dist/rules/no-reduce-accumulator-copy.js +99 -0
- package/dist/rules/no-reflect-apply.d.ts +2 -0
- package/dist/rules/no-reflect-apply.js +25 -0
- package/dist/rules/no-reflect-get.d.ts +2 -0
- package/dist/rules/no-reflect-get.js +25 -0
- package/dist/rules/no-runtime-typeof.d.ts +2 -0
- package/dist/rules/no-runtime-typeof.js +64 -0
- package/dist/rules/no-shape-in-symbol-names.d.ts +2 -0
- package/dist/rules/no-shape-in-symbol-names.js +40 -0
- package/dist/rules/no-unknown-parameters.d.ts +2 -0
- package/dist/rules/no-unknown-parameters.js +51 -0
- package/dist/rules/no-unknown-returns.d.ts +2 -0
- package/dist/rules/no-unknown-returns.js +58 -0
- package/dist/rules/no-unknown-type-aliases.d.ts +2 -0
- package/dist/rules/no-unknown-type-aliases.js +40 -0
- package/dist/rules/no-unsafe-dictionary-type.d.ts +2 -0
- package/dist/rules/no-unsafe-dictionary-type.js +134 -0
- package/dist/rules/no-widen-then-assert.d.ts +2 -0
- package/dist/rules/no-widen-then-assert.js +270 -0
- package/dist/rules/require-safety-comment-for-type-assertion.d.ts +2 -0
- package/dist/rules/require-safety-comment-for-type-assertion.js +100 -0
- package/dist/shared/array-method.d.ts +12 -0
- package/dist/shared/array-method.js +84 -0
- package/dist/shared/dictionary-types.d.ts +20 -0
- package/dist/shared/dictionary-types.js +377 -0
- package/dist/shared/function-parameters.d.ts +8 -0
- package/dist/shared/function-parameters.js +40 -0
- package/dist/shared/lexical-type-parameters.d.ts +5 -0
- package/dist/shared/lexical-type-parameters.js +47 -0
- package/dist/shared/reflect-method.d.ts +3 -0
- package/dist/shared/reflect-method.js +20 -0
- package/dist/shared/scope.d.ts +3 -0
- package/dist/shared/scope.js +11 -0
- package/dist/shared/type-alias-resolution.d.ts +23 -0
- package/dist/shared/type-alias-resolution.js +162 -0
- package/dist/tailwind/candidates.d.ts +14 -0
- package/dist/tailwind/candidates.js +24 -0
- package/dist/tailwind/design-system.d.ts +3 -0
- package/dist/tailwind/design-system.js +41 -0
- package/dist/tailwind/rule.d.ts +3 -0
- package/dist/tailwind/rule.js +133 -0
- package/dist/tailwind/worker.d.ts +1 -0
- package/dist/tailwind/worker.js +13 -0
- package/package.json +16 -5
- package/src/index.ts +0 -87
- package/src/rules/no-array-filter-map.ts +0 -28
- package/src/rules/no-call-only-assertions.ts +0 -94
- package/src/rules/no-chained-type-assertions.ts +0 -77
- package/src/rules/no-conditional-empty-object-spread.ts +0 -49
- package/src/rules/no-known-value-widening.ts +0 -415
- package/src/rules/no-module-mocking.ts +0 -80
- package/src/rules/no-object-parameters.ts +0 -83
- package/src/rules/no-pass-through-type-alias.ts +0 -52
- package/src/rules/no-reduce-accumulator-copy.ts +0 -109
- package/src/rules/no-reflect-apply.ts +0 -28
- package/src/rules/no-reflect-get.ts +0 -28
- package/src/rules/no-runtime-typeof.ts +0 -77
- package/src/rules/no-shape-in-symbol-names.ts +0 -46
- package/src/rules/no-unknown-parameters.ts +0 -69
- package/src/rules/no-unknown-returns.ts +0 -82
- package/src/rules/no-unknown-type-aliases.ts +0 -54
- package/src/rules/no-unsafe-dictionary-type.ts +0 -154
- package/src/rules/no-widen-then-assert.ts +0 -366
- package/src/rules/require-safety-comment-for-type-assertion.ts +0 -131
- package/src/shared/array-method.ts +0 -94
- package/src/shared/dictionary-types.ts +0 -515
- package/src/shared/function-parameters.ts +0 -49
- package/src/shared/lexical-type-parameters.ts +0 -61
- package/src/shared/reflect-method.ts +0 -24
- package/src/shared/scope.ts +0 -15
- package/src/shared/type-alias-resolution.ts +0 -250
- package/src/tailwind/candidates.ts +0 -38
- package/src/tailwind/design-system.ts +0 -59
- package/src/tailwind/rule.ts +0 -126
- package/src/tailwind/worker.ts +0 -38
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
const functionBoundaryTypes = new Set([
|
|
3
|
+
"ArrowFunctionExpression",
|
|
4
|
+
"FunctionDeclaration",
|
|
5
|
+
"FunctionExpression",
|
|
6
|
+
"TSDeclareFunction",
|
|
7
|
+
"TSEmptyBodyFunctionExpression",
|
|
8
|
+
]);
|
|
9
|
+
function unwrapExpressionParentheses(expression) {
|
|
10
|
+
let current = expression;
|
|
11
|
+
while (current.type === "ParenthesizedExpression")
|
|
12
|
+
current = current.expression;
|
|
13
|
+
return current;
|
|
14
|
+
}
|
|
15
|
+
function unwrapTypeParentheses(type) {
|
|
16
|
+
let current = type;
|
|
17
|
+
while (current.type === "TSParenthesizedType")
|
|
18
|
+
current = current.typeAnnotation;
|
|
19
|
+
return current;
|
|
20
|
+
}
|
|
21
|
+
function typeReferenceName(type) {
|
|
22
|
+
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
23
|
+
}
|
|
24
|
+
function isUnknownOrAnyType(type) {
|
|
25
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
26
|
+
return unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword";
|
|
27
|
+
}
|
|
28
|
+
function isBroadRecordKeyType(type) {
|
|
29
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
30
|
+
if (unwrapped.type === "TSStringKeyword" ||
|
|
31
|
+
unwrapped.type === "TSNumberKeyword" ||
|
|
32
|
+
unwrapped.type === "TSSymbolKeyword") {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (unwrapped.type === "TSUnionType")
|
|
36
|
+
return unwrapped.types.every(isBroadRecordKeyType);
|
|
37
|
+
return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey";
|
|
38
|
+
}
|
|
39
|
+
function isBroadRecordType(type) {
|
|
40
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
41
|
+
if (unwrapped.type === "TSTypeReference") {
|
|
42
|
+
if (typeReferenceName(unwrapped) === "Readonly") {
|
|
43
|
+
const [inner] = unwrapped.typeArguments?.params ?? [];
|
|
44
|
+
return inner !== undefined && isBroadRecordType(inner);
|
|
45
|
+
}
|
|
46
|
+
if (typeReferenceName(unwrapped) !== "Record")
|
|
47
|
+
return false;
|
|
48
|
+
const parameters = unwrapped.typeArguments?.params ?? [];
|
|
49
|
+
return (parameters.length === 2 &&
|
|
50
|
+
parameters[0] !== undefined &&
|
|
51
|
+
parameters[1] !== undefined &&
|
|
52
|
+
isBroadRecordKeyType(parameters[0]) &&
|
|
53
|
+
isUnknownOrAnyType(parameters[1]));
|
|
54
|
+
}
|
|
55
|
+
if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1)
|
|
56
|
+
return false;
|
|
57
|
+
const [member] = unwrapped.members;
|
|
58
|
+
const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : [];
|
|
59
|
+
return (member?.type === "TSIndexSignature" &&
|
|
60
|
+
member.parameters.length === 1 &&
|
|
61
|
+
parameter !== undefined &&
|
|
62
|
+
isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) &&
|
|
63
|
+
isUnknownOrAnyType(member.typeAnnotation.typeAnnotation));
|
|
64
|
+
}
|
|
65
|
+
function broadTypeKind(type) {
|
|
66
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
67
|
+
if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword")
|
|
68
|
+
return "top";
|
|
69
|
+
if (unwrapped.type === "TSObjectKeyword")
|
|
70
|
+
return "object";
|
|
71
|
+
return isBroadRecordType(unwrapped) ? "record" : null;
|
|
72
|
+
}
|
|
73
|
+
function assertedExpression(node) {
|
|
74
|
+
return unwrapExpressionParentheses(node.expression);
|
|
75
|
+
}
|
|
76
|
+
function assertionFromExpression(expression) {
|
|
77
|
+
const unwrapped = unwrapExpressionParentheses(expression);
|
|
78
|
+
return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion"
|
|
79
|
+
? unwrapped
|
|
80
|
+
: null;
|
|
81
|
+
}
|
|
82
|
+
function normalizedTypeText(sourceText, type) {
|
|
83
|
+
return sourceText.slice(type.start, type.end).replaceAll(/\s+/gu, "");
|
|
84
|
+
}
|
|
85
|
+
function typesHaveSameSyntax(sourceText, left, right) {
|
|
86
|
+
return (left !== null &&
|
|
87
|
+
normalizedTypeText(sourceText, unwrapTypeParentheses(left)) ===
|
|
88
|
+
normalizedTypeText(sourceText, unwrapTypeParentheses(right)));
|
|
89
|
+
}
|
|
90
|
+
function isDefinitelyObjectType(type) {
|
|
91
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
92
|
+
switch (unwrapped.type) {
|
|
93
|
+
case "TSArrayType":
|
|
94
|
+
case "TSConstructorType":
|
|
95
|
+
case "TSFunctionType":
|
|
96
|
+
case "TSMappedType":
|
|
97
|
+
case "TSObjectKeyword":
|
|
98
|
+
case "TSTupleType":
|
|
99
|
+
return true;
|
|
100
|
+
case "TSTypeLiteral":
|
|
101
|
+
return unwrapped.members.length > 0;
|
|
102
|
+
case "TSIntersectionType":
|
|
103
|
+
return unwrapped.types.every(isDefinitelyObjectType);
|
|
104
|
+
case "TSTypeOperator":
|
|
105
|
+
return unwrapped.operator === "readonly" && isDefinitelyObjectType(unwrapped.typeAnnotation);
|
|
106
|
+
default:
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function isDefinitelyNarrowerRecordType(type) {
|
|
111
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
112
|
+
if (unwrapped.type === "TSTypeLiteral") {
|
|
113
|
+
return unwrapped.members.some((member) => member.type !== "TSIndexSignature");
|
|
114
|
+
}
|
|
115
|
+
if (unwrapped.type !== "TSTypeReference")
|
|
116
|
+
return false;
|
|
117
|
+
if (typeReferenceName(unwrapped) === "Readonly") {
|
|
118
|
+
const [inner] = unwrapped.typeArguments?.params ?? [];
|
|
119
|
+
return inner !== undefined && isDefinitelyNarrowerRecordType(inner);
|
|
120
|
+
}
|
|
121
|
+
if (typeReferenceName(unwrapped) !== "Record")
|
|
122
|
+
return false;
|
|
123
|
+
const parameters = unwrapped.typeArguments?.params ?? [];
|
|
124
|
+
return (parameters.length === 2 && parameters[1] !== undefined && !isUnknownOrAnyType(parameters[1]));
|
|
125
|
+
}
|
|
126
|
+
function functionBoundary(node) {
|
|
127
|
+
let current = node.parent;
|
|
128
|
+
while (current !== null && current.type !== "Program") {
|
|
129
|
+
if (functionBoundaryTypes.has(current.type))
|
|
130
|
+
return current;
|
|
131
|
+
current = current.parent;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
function resolvedVariableForIdentifier(scopes, identifier) {
|
|
136
|
+
for (const scope of scopes) {
|
|
137
|
+
const reference = scope.references.find((candidate) => candidate.identifier.start === identifier.start &&
|
|
138
|
+
candidate.identifier.end === identifier.end);
|
|
139
|
+
if (reference !== undefined)
|
|
140
|
+
return reference.resolved;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
function variableDeclarator(variable) {
|
|
145
|
+
for (const definition of variable.defs) {
|
|
146
|
+
if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") {
|
|
147
|
+
return definition.node;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
function knownValueEvidence(expression, scopes, boundary, visitedVariables) {
|
|
153
|
+
const unwrapped = unwrapExpressionParentheses(expression);
|
|
154
|
+
if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") {
|
|
155
|
+
if (broadTypeKind(unwrapped.typeAnnotation) !== null)
|
|
156
|
+
return null;
|
|
157
|
+
return { type: unwrapped.typeAnnotation };
|
|
158
|
+
}
|
|
159
|
+
if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") {
|
|
160
|
+
return { type: null };
|
|
161
|
+
}
|
|
162
|
+
if (unwrapped.type === "ArrayExpression" ||
|
|
163
|
+
unwrapped.type === "ArrowFunctionExpression" ||
|
|
164
|
+
unwrapped.type === "ClassExpression" ||
|
|
165
|
+
unwrapped.type === "FunctionExpression" ||
|
|
166
|
+
unwrapped.type === "NewExpression" ||
|
|
167
|
+
unwrapped.type === "ObjectExpression") {
|
|
168
|
+
return { type: null };
|
|
169
|
+
}
|
|
170
|
+
if (unwrapped.type !== "Identifier")
|
|
171
|
+
return null;
|
|
172
|
+
const variable = resolvedVariableForIdentifier(scopes, unwrapped);
|
|
173
|
+
if (variable === null || visitedVariables.has(variable))
|
|
174
|
+
return null;
|
|
175
|
+
const annotatedIdentifier = variable.identifiers.find((identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== undefined);
|
|
176
|
+
const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation;
|
|
177
|
+
if (annotation !== undefined && annotatedIdentifier !== undefined) {
|
|
178
|
+
if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
return { type: annotation };
|
|
182
|
+
}
|
|
183
|
+
const declarator = variableDeclarator(variable);
|
|
184
|
+
if (declarator === null ||
|
|
185
|
+
declarator.parent.type !== "VariableDeclaration" ||
|
|
186
|
+
declarator.parent.kind !== "const" ||
|
|
187
|
+
declarator.init === null ||
|
|
188
|
+
variable.references.some((reference) => reference.isWrite() && !reference.init) ||
|
|
189
|
+
functionBoundary(declarator) !== boundary) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
return knownValueEvidence(declarator.init, scopes, boundary, new Set([...visitedVariables, variable]));
|
|
193
|
+
}
|
|
194
|
+
function widenedBinding(variable, scopes) {
|
|
195
|
+
const declarator = variableDeclarator(variable);
|
|
196
|
+
if (declarator === null ||
|
|
197
|
+
declarator.parent.type !== "VariableDeclaration" ||
|
|
198
|
+
declarator.parent.kind !== "const" ||
|
|
199
|
+
declarator.id.type !== "Identifier" ||
|
|
200
|
+
declarator.init === null ||
|
|
201
|
+
variable.references.some((reference) => reference.isWrite() && !reference.init)) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const boundary = functionBoundary(declarator);
|
|
205
|
+
const declaredType = declarator.id.typeAnnotation?.typeAnnotation;
|
|
206
|
+
const initializerAssertion = assertionFromExpression(declarator.init);
|
|
207
|
+
const initializerBroadKind = initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation);
|
|
208
|
+
const declaredBroadKind = declaredType === undefined ? null : broadTypeKind(declaredType);
|
|
209
|
+
const broadKind = declaredBroadKind ?? initializerBroadKind;
|
|
210
|
+
if (broadKind === null)
|
|
211
|
+
return null;
|
|
212
|
+
const originalExpression = initializerAssertion !== null && initializerBroadKind !== null
|
|
213
|
+
? assertedExpression(initializerAssertion)
|
|
214
|
+
: declarator.init;
|
|
215
|
+
const evidence = knownValueEvidence(originalExpression, scopes, boundary, new Set([variable]));
|
|
216
|
+
return evidence === null ? null : { broadKind, evidence, declaredAt: declarator.end, boundary };
|
|
217
|
+
}
|
|
218
|
+
function assertionIsNarrower(sourceText, broadKind, evidence, assertedType) {
|
|
219
|
+
if (broadTypeKind(assertedType) !== null)
|
|
220
|
+
return false;
|
|
221
|
+
if (broadKind === "top")
|
|
222
|
+
return true;
|
|
223
|
+
if (typesHaveSameSyntax(sourceText, evidence.type, assertedType))
|
|
224
|
+
return true;
|
|
225
|
+
if (broadKind === "object")
|
|
226
|
+
return isDefinitelyObjectType(assertedType);
|
|
227
|
+
return isDefinitelyNarrowerRecordType(assertedType);
|
|
228
|
+
}
|
|
229
|
+
/** Detect immutable local bindings that erase a known type and are later asserted back to a narrower type. */
|
|
230
|
+
export const noWidenThenAssertRule = defineRule({
|
|
231
|
+
meta: {
|
|
232
|
+
type: "problem",
|
|
233
|
+
docs: {
|
|
234
|
+
description: "Disallow local const flows that explicitly widen a known value before asserting the widened binding to a narrower type.",
|
|
235
|
+
},
|
|
236
|
+
messages: {
|
|
237
|
+
widenThenAssert: 'Binding "{{name}}" discards type evidence and later recreates it with an assertion. Keep the precise type from initialization through use; parse boundary input once.',
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
createOnce(context) {
|
|
241
|
+
let scopes = [];
|
|
242
|
+
const checkAssertion = (node) => {
|
|
243
|
+
const expression = assertedExpression(node);
|
|
244
|
+
if (expression.type !== "Identifier")
|
|
245
|
+
return;
|
|
246
|
+
const variable = resolvedVariableForIdentifier(scopes, expression);
|
|
247
|
+
if (variable === null)
|
|
248
|
+
return;
|
|
249
|
+
const widened = widenedBinding(variable, scopes);
|
|
250
|
+
if (widened === null ||
|
|
251
|
+
node.start <= widened.declaredAt ||
|
|
252
|
+
functionBoundary(node) !== widened.boundary ||
|
|
253
|
+
!assertionIsNarrower(context.sourceCode.text, widened.broadKind, widened.evidence, node.typeAnnotation)) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
context.report({
|
|
257
|
+
node,
|
|
258
|
+
messageId: "widenThenAssert",
|
|
259
|
+
data: { name: expression.name },
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
return {
|
|
263
|
+
Program() {
|
|
264
|
+
scopes = context.sourceCode.scopeManager.scopes;
|
|
265
|
+
},
|
|
266
|
+
TSAsExpression: checkAssertion,
|
|
267
|
+
TSTypeAssertion: checkAssertion,
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
const DEFAULT_SAFETY_MARKERS = ["SAFETY"];
|
|
3
|
+
const commentOwnerKinds = new Set([
|
|
4
|
+
"ExpressionStatement",
|
|
5
|
+
"PropertyDefinition",
|
|
6
|
+
"ReturnStatement",
|
|
7
|
+
"ThrowStatement",
|
|
8
|
+
"VariableDeclaration",
|
|
9
|
+
]);
|
|
10
|
+
function isConstAssertion(node) {
|
|
11
|
+
return (node.typeAnnotation.type === "TSTypeReference" &&
|
|
12
|
+
node.typeAnnotation.typeName.type === "Identifier" &&
|
|
13
|
+
node.typeAnnotation.typeName.name === "const");
|
|
14
|
+
}
|
|
15
|
+
function configuredSafetyMarkers(option) {
|
|
16
|
+
if (typeof option !== "object" || option === null || !("markers" in option)) {
|
|
17
|
+
return DEFAULT_SAFETY_MARKERS;
|
|
18
|
+
}
|
|
19
|
+
const configured = option.markers;
|
|
20
|
+
if (!Array.isArray(configured))
|
|
21
|
+
return DEFAULT_SAFETY_MARKERS;
|
|
22
|
+
const markers = configured.flatMap((marker) => typeof marker === "string" && marker.trim().length > 0 ? [marker.trim()] : []);
|
|
23
|
+
return markers.length > 0 ? markers : DEFAULT_SAFETY_MARKERS;
|
|
24
|
+
}
|
|
25
|
+
function markerPattern(markers) {
|
|
26
|
+
const alternation = markers
|
|
27
|
+
.map((marker) => marker.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw `\$&`))
|
|
28
|
+
.join("|");
|
|
29
|
+
return new RegExp(String.raw `(?:^|[^\p{L}\p{N}_])(?:${alternation})\s*:\s*\S`, "u");
|
|
30
|
+
}
|
|
31
|
+
function hasSafetyJustificationBefore(sourceCode, owner, assertion, pattern) {
|
|
32
|
+
return sourceCode
|
|
33
|
+
.getCommentsBefore(owner)
|
|
34
|
+
.some((comment) => comment.end <= assertion.start && pattern.test(comment.value));
|
|
35
|
+
}
|
|
36
|
+
function hasSafetyComment(sourceCode, node, pattern) {
|
|
37
|
+
let current = node;
|
|
38
|
+
while (true) {
|
|
39
|
+
if (hasSafetyJustificationBefore(sourceCode, current, node, pattern))
|
|
40
|
+
return true;
|
|
41
|
+
if (commentOwnerKinds.has(current.type)) {
|
|
42
|
+
const exportDeclaration = current.parent;
|
|
43
|
+
return (exportDeclaration.type === "ExportNamedDeclaration" &&
|
|
44
|
+
exportDeclaration.declaration === current &&
|
|
45
|
+
hasSafetyJustificationBefore(sourceCode, exportDeclaration, node, pattern));
|
|
46
|
+
}
|
|
47
|
+
if (current.parent.type === "Program")
|
|
48
|
+
return false;
|
|
49
|
+
current = current.parent;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Require every non-const type assertion to state the invariant TypeScript cannot express. */
|
|
53
|
+
export const requireSafetyCommentForTypeAssertionRule = defineRule({
|
|
54
|
+
meta: {
|
|
55
|
+
type: "problem",
|
|
56
|
+
docs: {
|
|
57
|
+
description: "Require a nearby SAFETY comment for every TypeScript type assertion except const assertions.",
|
|
58
|
+
},
|
|
59
|
+
messages: {
|
|
60
|
+
missingSafetyComment: "This type assertion has no `{{marker}}:` justification. State the checked invariant immediately before the assertion or its containing statement.",
|
|
61
|
+
},
|
|
62
|
+
schema: [
|
|
63
|
+
{
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
markers: {
|
|
67
|
+
type: "array",
|
|
68
|
+
items: { type: "string", minLength: 1 },
|
|
69
|
+
minItems: 1,
|
|
70
|
+
uniqueItems: true,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
defaultOptions: [{ markers: ["SAFETY"] }],
|
|
77
|
+
},
|
|
78
|
+
createOnce(context) {
|
|
79
|
+
const patterns = new Map();
|
|
80
|
+
const checkAssertion = (node) => {
|
|
81
|
+
if (isConstAssertion(node))
|
|
82
|
+
return;
|
|
83
|
+
const markers = configuredSafetyMarkers(context.options?.[0]);
|
|
84
|
+
const patternKey = markers.join("\u0000");
|
|
85
|
+
const pattern = patterns.get(patternKey) ?? markerPattern(markers);
|
|
86
|
+
patterns.set(patternKey, pattern);
|
|
87
|
+
if (hasSafetyComment(context.sourceCode, node, pattern))
|
|
88
|
+
return;
|
|
89
|
+
context.report({
|
|
90
|
+
node,
|
|
91
|
+
messageId: "missingSafetyComment",
|
|
92
|
+
data: { marker: markers[0] ?? DEFAULT_SAFETY_MARKERS[0] },
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
TSAsExpression: checkAssertion,
|
|
97
|
+
TSTypeAssertion: checkAssertion,
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ESTree, SourceCode, Variable } from "@oxlint/plugins";
|
|
2
|
+
/** Unwrap syntax-only wrappers when inspecting array methods and accumulator references. */
|
|
3
|
+
export declare function unwrapArrayExpression(node: ESTree.Node): ESTree.Node;
|
|
4
|
+
/** Resolve a local binding by scope, not by identifier spelling. */
|
|
5
|
+
export declare function resolveArrayBinding(sourceCode: SourceCode, node: ESTree.Node): Variable | null;
|
|
6
|
+
/** Read static method names, including computed string literals, without evaluating expressions. */
|
|
7
|
+
export declare function arrayMethodTarget(node: ESTree.Node): {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly object: ESTree.Node;
|
|
10
|
+
} | null;
|
|
11
|
+
/** Recognize local array evidence; unknown receivers and iterator pipelines are deliberately excluded. */
|
|
12
|
+
export declare function isKnownArrayExpression(sourceCode: SourceCode, node: ESTree.Node, visited?: Set<Variable>): boolean;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** Unwrap syntax-only wrappers when inspecting array methods and accumulator references. */
|
|
2
|
+
export function unwrapArrayExpression(node) {
|
|
3
|
+
while (node.type === "ParenthesizedExpression" ||
|
|
4
|
+
node.type === "ChainExpression" ||
|
|
5
|
+
node.type === "TSAsExpression" ||
|
|
6
|
+
node.type === "TSTypeAssertion" ||
|
|
7
|
+
node.type === "TSNonNullExpression" ||
|
|
8
|
+
node.type === "TSSatisfiesExpression") {
|
|
9
|
+
node = node.expression;
|
|
10
|
+
}
|
|
11
|
+
return node;
|
|
12
|
+
}
|
|
13
|
+
/** Resolve a local binding by scope, not by identifier spelling. */
|
|
14
|
+
export function resolveArrayBinding(sourceCode, node) {
|
|
15
|
+
node = unwrapArrayExpression(node);
|
|
16
|
+
if (node.type !== "Identifier")
|
|
17
|
+
return null;
|
|
18
|
+
let scope = sourceCode.getScope(node);
|
|
19
|
+
while (scope !== null) {
|
|
20
|
+
const variable = scope.set.get(node.name);
|
|
21
|
+
if (variable !== undefined)
|
|
22
|
+
return variable;
|
|
23
|
+
scope = scope.upper;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/** Read static method names, including computed string literals, without evaluating expressions. */
|
|
28
|
+
export function arrayMethodTarget(node) {
|
|
29
|
+
node = unwrapArrayExpression(node);
|
|
30
|
+
if (node.type !== "MemberExpression")
|
|
31
|
+
return null;
|
|
32
|
+
const property = node.property;
|
|
33
|
+
if (!node.computed && property.type === "Identifier") {
|
|
34
|
+
return { name: property.name, object: node.object };
|
|
35
|
+
}
|
|
36
|
+
if (node.computed && property.type === "Literal" && typeof property.value === "string") {
|
|
37
|
+
return { name: property.value, object: node.object };
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function isArrayAnnotation(type) {
|
|
42
|
+
if (type.type === "TSArrayType" || type.type === "TSTupleType")
|
|
43
|
+
return true;
|
|
44
|
+
if (type.type === "TSParenthesizedType")
|
|
45
|
+
return isArrayAnnotation(type.typeAnnotation);
|
|
46
|
+
if (type.type === "TSTypeOperator" && type.operator === "readonly") {
|
|
47
|
+
return isArrayAnnotation(type.typeAnnotation);
|
|
48
|
+
}
|
|
49
|
+
return (type.type === "TSTypeReference" && type.typeName.type === "Identifier" &&
|
|
50
|
+
(type.typeName.name === "Array" || type.typeName.name === "ReadonlyArray"));
|
|
51
|
+
}
|
|
52
|
+
/** Recognize local array evidence; unknown receivers and iterator pipelines are deliberately excluded. */
|
|
53
|
+
export function isKnownArrayExpression(sourceCode, node, visited = new Set()) {
|
|
54
|
+
node = unwrapArrayExpression(node);
|
|
55
|
+
if (node.type === "ArrayExpression")
|
|
56
|
+
return true;
|
|
57
|
+
if (node.type === "CallExpression") {
|
|
58
|
+
const method = arrayMethodTarget(node.callee);
|
|
59
|
+
return (method !== null &&
|
|
60
|
+
["map", "filter", "flatMap", "slice", "concat", "toSorted", "toReversed", "toSpliced"].includes(method.name) &&
|
|
61
|
+
isKnownArrayExpression(sourceCode, method.object, visited));
|
|
62
|
+
}
|
|
63
|
+
if (node.type !== "Identifier")
|
|
64
|
+
return false;
|
|
65
|
+
const variable = resolveArrayBinding(sourceCode, node);
|
|
66
|
+
if (variable === null || visited.has(variable))
|
|
67
|
+
return false;
|
|
68
|
+
visited.add(variable);
|
|
69
|
+
if (variable.references.some(reference => reference.isWrite() && !reference.init))
|
|
70
|
+
return false;
|
|
71
|
+
for (const identifier of variable.identifiers) {
|
|
72
|
+
const annotation = identifier.typeAnnotation?.typeAnnotation;
|
|
73
|
+
if (annotation !== undefined)
|
|
74
|
+
return isArrayAnnotation(annotation);
|
|
75
|
+
}
|
|
76
|
+
for (const definition of variable.defs) {
|
|
77
|
+
if (definition.type === "Variable" && definition.node.type === "VariableDeclarator" &&
|
|
78
|
+
definition.node.id.type === "Identifier" && definition.node.init !== null &&
|
|
79
|
+
definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const") {
|
|
80
|
+
return isKnownArrayExpression(sourceCode, definition.node.init, visited);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
2
|
+
import { type TypeAliasEnvironment as LexicalTypeAliasEnvironment } from "./type-alias-resolution.ts";
|
|
3
|
+
export type UnsafeDictionary = {
|
|
4
|
+
readonly kind: "unsafe-dictionary";
|
|
5
|
+
readonly unsafeValue: "any" | "empty-object" | "object" | "union" | "unknown";
|
|
6
|
+
};
|
|
7
|
+
export type WideningTargetKind = "anonymous object" | "generic container" | "object" | "open dictionary" | "unknown";
|
|
8
|
+
export type WideningTarget = {
|
|
9
|
+
readonly kind: WideningTargetKind;
|
|
10
|
+
};
|
|
11
|
+
export type TypeEnvironment = {
|
|
12
|
+
readonly interfaces: ReadonlyMap<string, readonly ESTree.TSInterfaceDeclaration[]>;
|
|
13
|
+
readonly typeAliases: LexicalTypeAliasEnvironment;
|
|
14
|
+
};
|
|
15
|
+
export declare function createTypeEnvironment(program: ESTree.Program, visitorKeys: Readonly<Record<string, readonly string[]>>): TypeEnvironment;
|
|
16
|
+
export declare function classifyUnsafeDictionaryValue(valueType: ESTree.TSType, environment: TypeEnvironment): UnsafeDictionary | null;
|
|
17
|
+
export declare function classifyUnsafeDictionary(type: ESTree.TSType, environment: TypeEnvironment): UnsafeDictionary | null;
|
|
18
|
+
export declare function classifyWideningTarget(type: ESTree.TSType, environment: TypeEnvironment): WideningTarget | null;
|
|
19
|
+
export declare function isPopulatedObjectExpression(expression: ESTree.Expression): boolean;
|
|
20
|
+
export declare function isKnownEvidenceExpression(expression: ESTree.Expression): boolean;
|