@timmo001/oxlint-rules 0.1.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/LICENSE +201 -0
- package/README.md +102 -0
- package/THIRD_PARTY_NOTICES.md +10 -0
- package/dist/cli.js +78 -0
- package/dist/configs/effect.js +52 -0
- package/dist/configs/recommended.js +35 -0
- package/dist/effect/index.js +106 -0
- package/dist/upstream/anti-slop.js +1838 -0
- package/dist/upstream/effect.js +59 -0
- package/package.json +61 -0
- package/skills/add-oxlint-rule/SKILL.md +31 -0
- package/skills/install-timmo-oxlint-rules/SKILL.md +45 -0
- package/skills/install-timmo-oxlint-rules/scripts/copy.mjs +30 -0
- package/src/cli.ts +35 -0
- package/src/configs/effect.ts +20 -0
- package/src/configs/recommended.ts +29 -0
- package/src/effect/index.ts +12 -0
- package/src/effect/rules/no-try-catch-in-effect-generators.ts +140 -0
- package/src/install/copy.ts +77 -0
- package/src/jsr/effect-config.ts +7 -0
- package/src/jsr/effect.ts +7 -0
- package/src/jsr/recommended.ts +7 -0
- package/src/jsr/upstream-anti-slop.ts +7 -0
- package/src/jsr/upstream-effect.ts +7 -0
- package/src/upstream/anti-slop.ts +1 -0
- package/src/upstream/effect.ts +1 -0
- package/vendor/anti-slop/LICENSE +21 -0
- package/vendor/anti-slop/README.md +299 -0
- package/vendor/anti-slop/src/effect/index.ts +13 -0
- package/vendor/anti-slop/src/effect/rules/no-service-constructor-imports.ts +52 -0
- package/vendor/anti-slop/src/index.ts +41 -0
- package/vendor/anti-slop/src/rules/no-chained-type-assertions.ts +77 -0
- package/vendor/anti-slop/src/rules/no-conditional-empty-object-spread.ts +49 -0
- package/vendor/anti-slop/src/rules/no-known-value-widening.ts +427 -0
- package/vendor/anti-slop/src/rules/no-module-mocking.ts +91 -0
- package/vendor/anti-slop/src/rules/no-object-parameters.ts +83 -0
- package/vendor/anti-slop/src/rules/no-reflect-apply.ts +28 -0
- package/vendor/anti-slop/src/rules/no-reflect-get.ts +28 -0
- package/vendor/anti-slop/src/rules/no-runtime-typeof.ts +77 -0
- package/vendor/anti-slop/src/rules/no-shape-in-symbol-names.ts +46 -0
- package/vendor/anti-slop/src/rules/no-unknown-parameters.ts +69 -0
- package/vendor/anti-slop/src/rules/no-unknown-returns.ts +82 -0
- package/vendor/anti-slop/src/rules/no-unknown-type-aliases.ts +54 -0
- package/vendor/anti-slop/src/rules/no-unsafe-dictionary-type.ts +154 -0
- package/vendor/anti-slop/src/rules/no-widen-then-assert.ts +366 -0
- package/vendor/anti-slop/src/rules/require-safety-comment-for-type-assertion.ts +131 -0
- package/vendor/anti-slop/src/shared/dictionary-types.ts +515 -0
- package/vendor/anti-slop/src/shared/function-parameters.ts +49 -0
- package/vendor/anti-slop/src/shared/lexical-type-parameters.ts +61 -0
- package/vendor/anti-slop/src/shared/reflect-method.ts +35 -0
- package/vendor/anti-slop/src/shared/type-alias-resolution.ts +250 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createTypeAliasEnvironment,
|
|
5
|
+
hasVisibleTypeBinding,
|
|
6
|
+
visibleTypeAlias,
|
|
7
|
+
type TypeAliasEnvironment as LexicalTypeAliasEnvironment,
|
|
8
|
+
} from "./type-alias-resolution.ts";
|
|
9
|
+
|
|
10
|
+
const BUILT_INS = new Set([
|
|
11
|
+
"Record",
|
|
12
|
+
"Readonly",
|
|
13
|
+
"Partial",
|
|
14
|
+
"Required",
|
|
15
|
+
"Pick",
|
|
16
|
+
"Omit",
|
|
17
|
+
"PropertyKey",
|
|
18
|
+
"NonNullable",
|
|
19
|
+
]);
|
|
20
|
+
const TRANSPARENT_WRAPPERS = new Set(["Readonly", "Partial", "Required", "NonNullable"]);
|
|
21
|
+
|
|
22
|
+
type TypeAliasEnvironment = ReadonlyMap<string, ESTree.TSType>;
|
|
23
|
+
|
|
24
|
+
type ResolvedType = {
|
|
25
|
+
readonly type: ESTree.TSType;
|
|
26
|
+
readonly substitutions: TypeAliasEnvironment;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type UnsafeDictionary = {
|
|
30
|
+
readonly kind: "unsafe-dictionary";
|
|
31
|
+
readonly unsafeValue: "any" | "empty-object" | "object" | "union" | "unknown";
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type WideningTargetKind =
|
|
35
|
+
| "anonymous object"
|
|
36
|
+
| "generic container"
|
|
37
|
+
| "object"
|
|
38
|
+
| "open dictionary"
|
|
39
|
+
| "unknown";
|
|
40
|
+
|
|
41
|
+
export type WideningTarget = {
|
|
42
|
+
readonly kind: WideningTargetKind;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type TypeEnvironment = {
|
|
46
|
+
readonly interfaces: ReadonlyMap<string, readonly ESTree.TSInterfaceDeclaration[]>;
|
|
47
|
+
readonly typeAliases: LexicalTypeAliasEnvironment;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function declaredStatement(statement: ESTree.Statement): ESTree.Node | null {
|
|
51
|
+
return statement.type === "ExportNamedDeclaration" ||
|
|
52
|
+
statement.type === "ExportDefaultDeclaration"
|
|
53
|
+
? (statement.declaration ?? null)
|
|
54
|
+
: statement;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createTypeEnvironment(
|
|
58
|
+
program: ESTree.Program,
|
|
59
|
+
visitorKeys: Readonly<Record<string, readonly string[]>>,
|
|
60
|
+
): TypeEnvironment {
|
|
61
|
+
const interfaces = new Map<string, ESTree.TSInterfaceDeclaration[]>();
|
|
62
|
+
|
|
63
|
+
for (const statement of program.body) {
|
|
64
|
+
const declaration = declaredStatement(statement);
|
|
65
|
+
if (declaration?.type !== "TSInterfaceDeclaration") continue;
|
|
66
|
+
const declarations = interfaces.get(declaration.id.name) ?? [];
|
|
67
|
+
declarations.push(declaration);
|
|
68
|
+
interfaces.set(declaration.id.name, declarations);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
interfaces,
|
|
73
|
+
typeAliases: createTypeAliasEnvironment(program, visitorKeys),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function typeReferenceName(type: ESTree.TSTypeReference): string | null {
|
|
78
|
+
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isBuiltIn(
|
|
82
|
+
name: string,
|
|
83
|
+
use: ESTree.Node,
|
|
84
|
+
environment: TypeEnvironment,
|
|
85
|
+
): boolean {
|
|
86
|
+
return (
|
|
87
|
+
BUILT_INS.has(name) &&
|
|
88
|
+
!hasVisibleTypeBinding(name, use, environment.typeAliases)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isUnappliedReferenceTo(type: ESTree.TSType, name: string): boolean {
|
|
93
|
+
const unwrapped = unwrapTransparentType(type);
|
|
94
|
+
return (
|
|
95
|
+
unwrapped.type === "TSTypeReference" &&
|
|
96
|
+
typeReferenceName(unwrapped) === name &&
|
|
97
|
+
(unwrapped.typeArguments === null ||
|
|
98
|
+
unwrapped.typeArguments === undefined ||
|
|
99
|
+
unwrapped.typeArguments.params.length === 0)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function unwrapTransparentType(type: ESTree.TSType): ESTree.TSType {
|
|
104
|
+
let current = type;
|
|
105
|
+
while (
|
|
106
|
+
current.type === "TSParenthesizedType" ||
|
|
107
|
+
(current.type === "TSTypeOperator" && current.operator === "readonly")
|
|
108
|
+
) {
|
|
109
|
+
current = current.typeAnnotation;
|
|
110
|
+
}
|
|
111
|
+
return current;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isNeverType(type: ESTree.TSType): boolean {
|
|
115
|
+
return unwrapTransparentType(type).type === "TSNeverKeyword";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isEffectivelyEmptyMember(member: ESTree.TSSignature): boolean {
|
|
119
|
+
return (
|
|
120
|
+
member.type === "TSPropertySignature" &&
|
|
121
|
+
member.optional === true &&
|
|
122
|
+
member.typeAnnotation !== null &&
|
|
123
|
+
member.typeAnnotation !== undefined &&
|
|
124
|
+
isNeverType(member.typeAnnotation.typeAnnotation)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isEffectivelyEmptyTypeLiteral(type: ESTree.TSTypeLiteral): boolean {
|
|
129
|
+
return type.members.length === 0 || type.members.every(isEffectivelyEmptyMember);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isEffectivelyEmptyInterface(
|
|
133
|
+
declarations: readonly ESTree.TSInterfaceDeclaration[],
|
|
134
|
+
): boolean {
|
|
135
|
+
if (declarations.length !== 1) return false;
|
|
136
|
+
const [type] = declarations;
|
|
137
|
+
return (
|
|
138
|
+
type !== undefined &&
|
|
139
|
+
type.extends.length === 0 &&
|
|
140
|
+
(type.body.body.length === 0 || type.body.body.every(isEffectivelyEmptyMember))
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolvedSubstitutionArgument(
|
|
145
|
+
type: ESTree.TSType,
|
|
146
|
+
base: TypeAliasEnvironment,
|
|
147
|
+
resolving: ReadonlySet<string> = new Set(),
|
|
148
|
+
): ESTree.TSType {
|
|
149
|
+
const unwrapped = unwrapTransparentType(type);
|
|
150
|
+
if (unwrapped.type !== "TSTypeReference") return type;
|
|
151
|
+
const name = typeReferenceName(unwrapped);
|
|
152
|
+
if (name === null || resolving.has(name)) return type;
|
|
153
|
+
const substitution = base.get(name);
|
|
154
|
+
if (substitution === undefined) return type;
|
|
155
|
+
const nextResolving = new Set(resolving);
|
|
156
|
+
nextResolving.add(name);
|
|
157
|
+
return resolvedSubstitutionArgument(substitution, base, nextResolving);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function aliasSubstitution(
|
|
161
|
+
alias: ESTree.TSTypeAliasDeclaration,
|
|
162
|
+
type: ESTree.TSTypeReference,
|
|
163
|
+
base: TypeAliasEnvironment,
|
|
164
|
+
): TypeAliasEnvironment | null {
|
|
165
|
+
const parameters = alias.typeParameters?.params ?? [];
|
|
166
|
+
const arguments_ = type.typeArguments?.params ?? [];
|
|
167
|
+
const next = new Map(base);
|
|
168
|
+
for (const [index, parameter] of parameters.entries()) {
|
|
169
|
+
const argument = arguments_[index] ?? parameter.default;
|
|
170
|
+
if (argument === null || argument === undefined) return null;
|
|
171
|
+
next.set(parameter.name.name, resolvedSubstitutionArgument(argument, next));
|
|
172
|
+
}
|
|
173
|
+
return next;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function unsafeDirectValue(
|
|
177
|
+
type: ESTree.TSType,
|
|
178
|
+
environment: TypeEnvironment,
|
|
179
|
+
substitutions: TypeAliasEnvironment,
|
|
180
|
+
resolvingAliases: ReadonlySet<string>,
|
|
181
|
+
): UnsafeDictionary["unsafeValue"] | null {
|
|
182
|
+
const unwrapped = unwrapTransparentType(type);
|
|
183
|
+
if (unwrapped.type === "TSUnknownKeyword") return "unknown";
|
|
184
|
+
if (unwrapped.type === "TSAnyKeyword") return "any";
|
|
185
|
+
if (unwrapped.type === "TSObjectKeyword") return "object";
|
|
186
|
+
if (unwrapped.type === "TSTypeLiteral" && isEffectivelyEmptyTypeLiteral(unwrapped))
|
|
187
|
+
return "empty-object";
|
|
188
|
+
if (unwrapped.type === "TSUnionType") {
|
|
189
|
+
return unwrapped.types.some(
|
|
190
|
+
(member) => unsafeDirectValue(member, environment, substitutions, resolvingAliases) !== null,
|
|
191
|
+
)
|
|
192
|
+
? "union"
|
|
193
|
+
: null;
|
|
194
|
+
}
|
|
195
|
+
if (unwrapped.type === "TSIntersectionType") {
|
|
196
|
+
const unsafeMembers = unwrapped.types.map((member) =>
|
|
197
|
+
unsafeDirectValue(member, environment, substitutions, resolvingAliases),
|
|
198
|
+
);
|
|
199
|
+
if (unsafeMembers.includes("any")) return "any";
|
|
200
|
+
return unsafeMembers.length > 0 && unsafeMembers.every((member) => member !== null)
|
|
201
|
+
? unsafeMembers[0]
|
|
202
|
+
: null;
|
|
203
|
+
}
|
|
204
|
+
if (unwrapped.type !== "TSTypeReference") return null;
|
|
205
|
+
const name = typeReferenceName(unwrapped);
|
|
206
|
+
if (name === null) return null;
|
|
207
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
|
|
208
|
+
const wrapped = unwrapped.typeArguments?.params[0];
|
|
209
|
+
return wrapped === undefined
|
|
210
|
+
? null
|
|
211
|
+
: unsafeDirectValue(wrapped, environment, substitutions, resolvingAliases);
|
|
212
|
+
}
|
|
213
|
+
const substitution = substitutions.get(name);
|
|
214
|
+
if (substitution !== undefined) {
|
|
215
|
+
return isUnappliedReferenceTo(substitution, name)
|
|
216
|
+
? null
|
|
217
|
+
: unsafeDirectValue(substitution, environment, substitutions, resolvingAliases);
|
|
218
|
+
}
|
|
219
|
+
const interfaceDeclarations = environment.interfaces.get(name);
|
|
220
|
+
if (interfaceDeclarations !== undefined) {
|
|
221
|
+
return isEffectivelyEmptyInterface(interfaceDeclarations) ? "empty-object" : null;
|
|
222
|
+
}
|
|
223
|
+
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
224
|
+
if (alias === null || resolvingAliases.has(name)) return null;
|
|
225
|
+
const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
|
|
226
|
+
if (nextSubstitutions === null) return null;
|
|
227
|
+
const nextResolving = new Set(resolvingAliases);
|
|
228
|
+
nextResolving.add(name);
|
|
229
|
+
return unsafeDirectValue(alias.typeAnnotation, environment, nextSubstitutions, nextResolving);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function dictionaryValueTypes(
|
|
233
|
+
type: ESTree.TSType,
|
|
234
|
+
environment: TypeEnvironment,
|
|
235
|
+
substitutions: TypeAliasEnvironment,
|
|
236
|
+
resolvingAliases: ReadonlySet<string>,
|
|
237
|
+
): readonly ResolvedType[] {
|
|
238
|
+
const unwrapped = unwrapTransparentType(type);
|
|
239
|
+
|
|
240
|
+
if (unwrapped.type === "TSTypeLiteral") {
|
|
241
|
+
return unwrapped.members.flatMap((member): readonly ResolvedType[] =>
|
|
242
|
+
member.type === "TSIndexSignature" && member.typeAnnotation !== null
|
|
243
|
+
? [{ type: member.typeAnnotation.typeAnnotation, substitutions }]
|
|
244
|
+
: [],
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (unwrapped.type === "TSMappedType") {
|
|
249
|
+
return unwrapped.typeAnnotation === null
|
|
250
|
+
? []
|
|
251
|
+
: [{ type: unwrapped.typeAnnotation, substitutions }];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (unwrapped.type !== "TSTypeReference") return [];
|
|
255
|
+
const name = typeReferenceName(unwrapped);
|
|
256
|
+
if (name === null) return [];
|
|
257
|
+
|
|
258
|
+
const substitution = substitutions.get(name);
|
|
259
|
+
if (substitution !== undefined) {
|
|
260
|
+
return isUnappliedReferenceTo(substitution, name)
|
|
261
|
+
? []
|
|
262
|
+
: dictionaryValueTypes(substitution, environment, substitutions, resolvingAliases);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
|
|
266
|
+
const wrapped = unwrapped.typeArguments?.params[0];
|
|
267
|
+
return wrapped === undefined
|
|
268
|
+
? []
|
|
269
|
+
: dictionaryValueTypes(wrapped, environment, substitutions, resolvingAliases);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (name === "Record" && isBuiltIn(name, unwrapped, environment)) {
|
|
273
|
+
const value = unwrapped.typeArguments?.params[1] ?? null;
|
|
274
|
+
return value === null ? [] : [{ type: value, substitutions }];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (
|
|
278
|
+
(name === "Pick" || name === "Omit") &&
|
|
279
|
+
isBuiltIn(name, unwrapped, environment)
|
|
280
|
+
) {
|
|
281
|
+
const source = unwrapped.typeArguments?.params[0];
|
|
282
|
+
return source === undefined
|
|
283
|
+
? []
|
|
284
|
+
: dictionaryValueTypes(source, environment, substitutions, resolvingAliases);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
288
|
+
if (alias === null || resolvingAliases.has(name)) return [];
|
|
289
|
+
const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
|
|
290
|
+
if (nextSubstitutions === null) return [];
|
|
291
|
+
const nextResolving = new Set(resolvingAliases);
|
|
292
|
+
nextResolving.add(name);
|
|
293
|
+
return dictionaryValueTypes(alias.typeAnnotation, environment, nextSubstitutions, nextResolving);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function classifyUnsafeDictionaryValue(
|
|
297
|
+
valueType: ESTree.TSType,
|
|
298
|
+
environment: TypeEnvironment,
|
|
299
|
+
): UnsafeDictionary | null {
|
|
300
|
+
const unsafeValue = unsafeDirectValue(valueType, environment, new Map(), new Set());
|
|
301
|
+
return unsafeValue === null ? null : { kind: "unsafe-dictionary", unsafeValue };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function classifyUnsafeDictionary(
|
|
305
|
+
type: ESTree.TSType,
|
|
306
|
+
environment: TypeEnvironment,
|
|
307
|
+
): UnsafeDictionary | null {
|
|
308
|
+
for (const valueType of dictionaryValueTypes(type, environment, new Map(), new Set())) {
|
|
309
|
+
const unsafeValue = unsafeDirectValue(
|
|
310
|
+
valueType.type,
|
|
311
|
+
environment,
|
|
312
|
+
valueType.substitutions,
|
|
313
|
+
new Set(),
|
|
314
|
+
);
|
|
315
|
+
if (unsafeValue !== null) return { kind: "unsafe-dictionary", unsafeValue };
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function classifyWideningTarget(
|
|
321
|
+
type: ESTree.TSType,
|
|
322
|
+
environment: TypeEnvironment,
|
|
323
|
+
): WideningTarget | null {
|
|
324
|
+
const unwrapped = unwrapTransparentType(type);
|
|
325
|
+
if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
|
|
326
|
+
if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
|
|
327
|
+
if (unwrapped.type === "TSTypeLiteral") {
|
|
328
|
+
return unwrapped.members.some((member) => member.type === "TSIndexSignature")
|
|
329
|
+
? { kind: "open dictionary" }
|
|
330
|
+
: unwrapped.members.length > 0
|
|
331
|
+
? { kind: "anonymous object" }
|
|
332
|
+
: null;
|
|
333
|
+
}
|
|
334
|
+
if (unwrapped.type === "TSMappedType") return { kind: "open dictionary" };
|
|
335
|
+
if (unwrapped.type !== "TSTypeReference") return null;
|
|
336
|
+
const name = typeReferenceName(unwrapped);
|
|
337
|
+
if (name === null) return null;
|
|
338
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
|
|
339
|
+
const wrapped = unwrapped.typeArguments?.params[0];
|
|
340
|
+
return wrapped === undefined ? null : classifyWideningTarget(wrapped, environment);
|
|
341
|
+
}
|
|
342
|
+
if (name === "Record" && isBuiltIn(name, unwrapped, environment)) {
|
|
343
|
+
return hasBroadRecordKey(unwrapped, environment, new Map())
|
|
344
|
+
? { kind: "open dictionary" }
|
|
345
|
+
: null;
|
|
346
|
+
}
|
|
347
|
+
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
348
|
+
if (alias === null) return null;
|
|
349
|
+
if ((alias.typeParameters?.params.length ?? 0) > 0) {
|
|
350
|
+
const substitutions = aliasSubstitution(alias, unwrapped, new Map());
|
|
351
|
+
const resolved =
|
|
352
|
+
substitutions === null
|
|
353
|
+
? null
|
|
354
|
+
: classifyAliasBroadTarget(
|
|
355
|
+
alias.typeAnnotation,
|
|
356
|
+
environment,
|
|
357
|
+
substitutions,
|
|
358
|
+
new Set([name]),
|
|
359
|
+
);
|
|
360
|
+
return resolved?.kind === "open dictionary" ? { kind: "generic container" } : null;
|
|
361
|
+
}
|
|
362
|
+
const substitutions = aliasSubstitution(alias, unwrapped, new Map());
|
|
363
|
+
if (substitutions === null) return null;
|
|
364
|
+
const resolved = classifyAliasBroadTarget(
|
|
365
|
+
alias.typeAnnotation,
|
|
366
|
+
environment,
|
|
367
|
+
substitutions,
|
|
368
|
+
new Set([name]),
|
|
369
|
+
);
|
|
370
|
+
return resolved;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function hasBroadRecordKey(
|
|
374
|
+
type: ESTree.TSTypeReference,
|
|
375
|
+
environment: TypeEnvironment,
|
|
376
|
+
substitutions: TypeAliasEnvironment,
|
|
377
|
+
): boolean {
|
|
378
|
+
const key = type.typeArguments?.params[0];
|
|
379
|
+
return key === undefined || isBroadMappedKey(key, environment, substitutions);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function isBroadMappedKey(
|
|
383
|
+
type: ESTree.TSType,
|
|
384
|
+
environment: TypeEnvironment,
|
|
385
|
+
substitutions: TypeAliasEnvironment,
|
|
386
|
+
visitedAliases: ReadonlySet<string> = new Set(),
|
|
387
|
+
): boolean {
|
|
388
|
+
const unwrapped = unwrapTransparentType(type);
|
|
389
|
+
if (
|
|
390
|
+
unwrapped.type === "TSStringKeyword" ||
|
|
391
|
+
unwrapped.type === "TSNumberKeyword" ||
|
|
392
|
+
unwrapped.type === "TSSymbolKeyword"
|
|
393
|
+
) {
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
if (unwrapped.type === "TSUnionType") {
|
|
397
|
+
return unwrapped.types.some((member) =>
|
|
398
|
+
isBroadMappedKey(member, environment, substitutions, visitedAliases),
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
if (unwrapped.type !== "TSTypeReference") return false;
|
|
402
|
+
const name = typeReferenceName(unwrapped);
|
|
403
|
+
if (name === null) return false;
|
|
404
|
+
const substitution = substitutions.get(name);
|
|
405
|
+
if (substitution !== undefined && !isUnappliedReferenceTo(substitution, name)) {
|
|
406
|
+
return isBroadMappedKey(substitution, environment, substitutions, visitedAliases);
|
|
407
|
+
}
|
|
408
|
+
if (name === "PropertyKey" && isBuiltIn(name, unwrapped, environment)) return true;
|
|
409
|
+
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
410
|
+
if (
|
|
411
|
+
alias === null ||
|
|
412
|
+
(alias.typeParameters?.params.length ?? 0) > 0 ||
|
|
413
|
+
visitedAliases.has(name)
|
|
414
|
+
) {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
const nextVisited = new Set(visitedAliases);
|
|
418
|
+
nextVisited.add(name);
|
|
419
|
+
return isBroadMappedKey(alias.typeAnnotation, environment, substitutions, nextVisited);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function classifyAliasBroadTarget(
|
|
423
|
+
type: ESTree.TSType,
|
|
424
|
+
environment: TypeEnvironment,
|
|
425
|
+
substitutions: TypeAliasEnvironment,
|
|
426
|
+
resolvingAliases: ReadonlySet<string>,
|
|
427
|
+
): WideningTarget | null {
|
|
428
|
+
const unwrapped = unwrapTransparentType(type);
|
|
429
|
+
if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
|
|
430
|
+
if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
|
|
431
|
+
if (unwrapped.type === "TSTypeLiteral") {
|
|
432
|
+
return unwrapped.members.some((member) => member.type === "TSIndexSignature")
|
|
433
|
+
? { kind: "open dictionary" }
|
|
434
|
+
: null;
|
|
435
|
+
}
|
|
436
|
+
if (unwrapped.type === "TSMappedType") {
|
|
437
|
+
return isBroadMappedKey(unwrapped.constraint, environment, substitutions)
|
|
438
|
+
? { kind: "open dictionary" }
|
|
439
|
+
: null;
|
|
440
|
+
}
|
|
441
|
+
if (unwrapped.type !== "TSTypeReference") return null;
|
|
442
|
+
const name = typeReferenceName(unwrapped);
|
|
443
|
+
if (name === null) return null;
|
|
444
|
+
const substitution = substitutions.get(name);
|
|
445
|
+
if (substitution !== undefined) {
|
|
446
|
+
return isUnappliedReferenceTo(substitution, name)
|
|
447
|
+
? null
|
|
448
|
+
: classifyAliasBroadTarget(
|
|
449
|
+
substitution,
|
|
450
|
+
environment,
|
|
451
|
+
substitutions,
|
|
452
|
+
resolvingAliases,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
|
|
456
|
+
const wrapped = unwrapped.typeArguments?.params[0];
|
|
457
|
+
return wrapped === undefined
|
|
458
|
+
? null
|
|
459
|
+
: classifyAliasBroadTarget(wrapped, environment, substitutions, resolvingAliases);
|
|
460
|
+
}
|
|
461
|
+
if (name === "Record" && isBuiltIn(name, unwrapped, environment)) {
|
|
462
|
+
return hasBroadRecordKey(unwrapped, environment, substitutions)
|
|
463
|
+
? { kind: "open dictionary" }
|
|
464
|
+
: null;
|
|
465
|
+
}
|
|
466
|
+
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
467
|
+
if (alias === null || resolvingAliases.has(name)) return null;
|
|
468
|
+
const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
|
|
469
|
+
if (nextSubstitutions === null) return null;
|
|
470
|
+
const nextResolving = new Set(resolvingAliases);
|
|
471
|
+
nextResolving.add(name);
|
|
472
|
+
return classifyAliasBroadTarget(
|
|
473
|
+
alias.typeAnnotation,
|
|
474
|
+
environment,
|
|
475
|
+
nextSubstitutions,
|
|
476
|
+
nextResolving,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function isPopulatedObjectExpression(expression: ESTree.Expression): boolean {
|
|
481
|
+
let current = expression;
|
|
482
|
+
while (
|
|
483
|
+
current.type === "ParenthesizedExpression" ||
|
|
484
|
+
current.type === "TSAsExpression" ||
|
|
485
|
+
current.type === "TSTypeAssertion" ||
|
|
486
|
+
current.type === "TSNonNullExpression"
|
|
487
|
+
) {
|
|
488
|
+
current = current.expression;
|
|
489
|
+
}
|
|
490
|
+
return current.type === "ObjectExpression" && current.properties.length > 0;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function isKnownEvidenceExpression(expression: ESTree.Expression): boolean {
|
|
494
|
+
let current = expression;
|
|
495
|
+
while (
|
|
496
|
+
current.type === "ParenthesizedExpression" ||
|
|
497
|
+
current.type === "TSAsExpression" ||
|
|
498
|
+
current.type === "TSTypeAssertion" ||
|
|
499
|
+
current.type === "TSNonNullExpression" ||
|
|
500
|
+
current.type === "TSSatisfiesExpression"
|
|
501
|
+
) {
|
|
502
|
+
current = current.expression;
|
|
503
|
+
}
|
|
504
|
+
if (current.type === "ObjectExpression") return true;
|
|
505
|
+
return (
|
|
506
|
+
current.type === "ArrayExpression" ||
|
|
507
|
+
current.type === "ArrowFunctionExpression" ||
|
|
508
|
+
current.type === "ClassExpression" ||
|
|
509
|
+
current.type === "FunctionExpression" ||
|
|
510
|
+
current.type === "NewExpression" ||
|
|
511
|
+
current.type === "Literal" ||
|
|
512
|
+
current.type === "TemplateLiteral" ||
|
|
513
|
+
current.type === "UnaryExpression"
|
|
514
|
+
);
|
|
515
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ESTree, SourceCode } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
export type FunctionParameter = ESTree.ParamPattern;
|
|
4
|
+
|
|
5
|
+
/** Return whether a type is or contains TypeScript's absorbing unknown top type. */
|
|
6
|
+
export function containsUnknownType(type: ESTree.TSType): boolean {
|
|
7
|
+
if (type.type === "TSUnknownKeyword") return true;
|
|
8
|
+
if (type.type === "TSParenthesizedType") return containsUnknownType(type.typeAnnotation);
|
|
9
|
+
return type.type === "TSUnionType" && type.types.some(containsUnknownType);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Return the TypeScript annotation attached to a function parameter or its wrapped binding. */
|
|
13
|
+
export function functionParameterTypeAnnotation(
|
|
14
|
+
parameter: FunctionParameter,
|
|
15
|
+
): ESTree.TSTypeAnnotation | null | undefined {
|
|
16
|
+
if (parameter.type === "TSParameterProperty") {
|
|
17
|
+
return functionParameterTypeAnnotation(parameter.parameter);
|
|
18
|
+
}
|
|
19
|
+
if (parameter.type === "RestElement") {
|
|
20
|
+
return parameter.typeAnnotation ?? functionParameterTypeAnnotation(parameter.argument);
|
|
21
|
+
}
|
|
22
|
+
if (parameter.type === "AssignmentPattern") {
|
|
23
|
+
return parameter.typeAnnotation ?? functionParameterTypeAnnotation(parameter.left);
|
|
24
|
+
}
|
|
25
|
+
return parameter.typeAnnotation;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Return only a function parameter's local binding, excluding its annotation and default value. */
|
|
29
|
+
export function functionParameterBindingName(
|
|
30
|
+
parameter: FunctionParameter,
|
|
31
|
+
sourceCode: SourceCode,
|
|
32
|
+
): string {
|
|
33
|
+
if (parameter.type === "TSParameterProperty") {
|
|
34
|
+
return functionParameterBindingName(parameter.parameter, sourceCode);
|
|
35
|
+
}
|
|
36
|
+
if (parameter.type === "AssignmentPattern") {
|
|
37
|
+
return functionParameterBindingName(parameter.left, sourceCode);
|
|
38
|
+
}
|
|
39
|
+
if (parameter.type === "RestElement") {
|
|
40
|
+
return functionParameterBindingName(parameter.argument, sourceCode);
|
|
41
|
+
}
|
|
42
|
+
if (parameter.type === "Identifier") return parameter.name;
|
|
43
|
+
|
|
44
|
+
const sourceText = sourceCode.getText(parameter);
|
|
45
|
+
const annotationStart = parameter.typeAnnotation?.start;
|
|
46
|
+
return annotationStart === undefined
|
|
47
|
+
? sourceText
|
|
48
|
+
: sourceText.slice(0, annotationStart - parameter.start).trimEnd();
|
|
49
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
type VisitorKeys = Readonly<Record<string, readonly string[]>>;
|
|
4
|
+
|
|
5
|
+
function isNode(value: unknown): value is ESTree.Node {
|
|
6
|
+
return (
|
|
7
|
+
typeof value === "object" &&
|
|
8
|
+
value !== null &&
|
|
9
|
+
"type" in value &&
|
|
10
|
+
typeof value.type === "string"
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function collectInferTypeParameterNames(
|
|
15
|
+
node: ESTree.Node,
|
|
16
|
+
visitorKeys: VisitorKeys,
|
|
17
|
+
names: Set<string>,
|
|
18
|
+
): void {
|
|
19
|
+
if (node.type === "TSInferType") names.add(node.typeParameter.name.name);
|
|
20
|
+
const record = node as unknown as Readonly<Record<string, unknown>>;
|
|
21
|
+
for (const key of visitorKeys[node.type] ?? []) {
|
|
22
|
+
const value = record[key];
|
|
23
|
+
if (isNode(value)) {
|
|
24
|
+
collectInferTypeParameterNames(value, visitorKeys, names);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (!Array.isArray(value)) continue;
|
|
28
|
+
for (const child of value) {
|
|
29
|
+
if (isNode(child)) collectInferTypeParameterNames(child, visitorKeys, names);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Collect type binders that are in scope at a node and can shadow module aliases. */
|
|
35
|
+
export function lexicalTypeParameterNames(
|
|
36
|
+
node: ESTree.Node,
|
|
37
|
+
visitorKeys: VisitorKeys,
|
|
38
|
+
): ReadonlySet<string> {
|
|
39
|
+
const names = new Set<string>();
|
|
40
|
+
let descendant: ESTree.Node = node;
|
|
41
|
+
let current: ESTree.Node | null = node;
|
|
42
|
+
while (current !== null && current.type !== "Program") {
|
|
43
|
+
if ("typeParameters" in current) {
|
|
44
|
+
for (const parameter of current.typeParameters?.params ?? []) {
|
|
45
|
+
names.add(parameter.name.name);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (
|
|
49
|
+
current.type === "TSMappedType" &&
|
|
50
|
+
(descendant === current.nameType || descendant === current.typeAnnotation)
|
|
51
|
+
) {
|
|
52
|
+
names.add(current.key.name);
|
|
53
|
+
}
|
|
54
|
+
if (current.type === "TSConditionalType" && descendant === current.trueType) {
|
|
55
|
+
collectInferTypeParameterNames(current.extendsType, visitorKeys, names);
|
|
56
|
+
}
|
|
57
|
+
descendant = current;
|
|
58
|
+
current = current.parent;
|
|
59
|
+
}
|
|
60
|
+
return names;
|
|
61
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
function resolveVariable(
|
|
4
|
+
sourceCode: SourceCode,
|
|
5
|
+
identifier: ESTree.IdentifierReference,
|
|
6
|
+
): Variable | null {
|
|
7
|
+
let scope: Scope | null = sourceCode.getScope(identifier);
|
|
8
|
+
while (scope !== null) {
|
|
9
|
+
const variable = scope.set.get(identifier.name);
|
|
10
|
+
if (variable !== undefined) return variable;
|
|
11
|
+
scope = scope.upper;
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isGlobalReflect(sourceCode: SourceCode, expression: ESTree.Expression): boolean {
|
|
17
|
+
if (expression.type !== "Identifier" || expression.name !== "Reflect") return false;
|
|
18
|
+
if (sourceCode.isGlobalReference(expression)) return true;
|
|
19
|
+
const variable = resolveVariable(sourceCode, expression);
|
|
20
|
+
return variable === null || variable.defs.length === 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Reports whether a call target names one method on the global Reflect object. */
|
|
24
|
+
export function isGlobalReflectMethodCall(
|
|
25
|
+
sourceCode: SourceCode,
|
|
26
|
+
callee: ESTree.Expression,
|
|
27
|
+
methodName: string,
|
|
28
|
+
): boolean {
|
|
29
|
+
if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
|
|
30
|
+
if (!isGlobalReflect(sourceCode, callee.object)) return false;
|
|
31
|
+
const property = callee.property;
|
|
32
|
+
return callee.computed
|
|
33
|
+
? property.type === "Literal" && property.value === methodName
|
|
34
|
+
: property.type === "Identifier" && property.name === methodName;
|
|
35
|
+
}
|