@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,250 @@
|
|
|
1
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import { lexicalTypeParameterNames } from "./lexical-type-parameters.ts";
|
|
4
|
+
|
|
5
|
+
type VisitorKeys = Readonly<Record<string, readonly string[]>>;
|
|
6
|
+
type TypeScope = ESTree.Node;
|
|
7
|
+
|
|
8
|
+
type TypeBinding = {
|
|
9
|
+
readonly alias: ESTree.TSTypeAliasDeclaration | null;
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly scope: TypeScope;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type Substitution = {
|
|
15
|
+
readonly substitutions: Substitutions;
|
|
16
|
+
readonly type: ESTree.TSType;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type Substitutions = ReadonlyMap<string, Substitution>;
|
|
20
|
+
|
|
21
|
+
export type TypeAliasEnvironment = {
|
|
22
|
+
readonly aliases: readonly ESTree.TSTypeAliasDeclaration[];
|
|
23
|
+
readonly bindingsByName: ReadonlyMap<string, readonly TypeBinding[]>;
|
|
24
|
+
readonly visitorKeys: VisitorKeys;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ResolvedTypeMatcher = (
|
|
28
|
+
type: ESTree.TSType,
|
|
29
|
+
matches: (child: ESTree.TSType) => boolean,
|
|
30
|
+
) => boolean;
|
|
31
|
+
|
|
32
|
+
const environmentsByProgram = new WeakMap<ESTree.Program, TypeAliasEnvironment>();
|
|
33
|
+
|
|
34
|
+
function isNode(value: unknown): value is ESTree.Node {
|
|
35
|
+
return (
|
|
36
|
+
typeof value === "object" &&
|
|
37
|
+
value !== null &&
|
|
38
|
+
"type" in value &&
|
|
39
|
+
typeof value.type === "string"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function enclosingTypeScope(node: ESTree.Node): TypeScope {
|
|
44
|
+
let current: ESTree.Node | null = node.parent;
|
|
45
|
+
while (current !== null) {
|
|
46
|
+
if (
|
|
47
|
+
current.type === "Program" ||
|
|
48
|
+
current.type === "BlockStatement" ||
|
|
49
|
+
current.type === "TSModuleBlock" ||
|
|
50
|
+
current.type === "StaticBlock" ||
|
|
51
|
+
current.type === "SwitchStatement"
|
|
52
|
+
) {
|
|
53
|
+
return current;
|
|
54
|
+
}
|
|
55
|
+
current = current.parent;
|
|
56
|
+
}
|
|
57
|
+
return node;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function declaredTypeBinding(node: ESTree.Node): {
|
|
61
|
+
readonly alias: ESTree.TSTypeAliasDeclaration | null;
|
|
62
|
+
readonly name: string;
|
|
63
|
+
} | null {
|
|
64
|
+
if (node.type === "TSTypeAliasDeclaration") {
|
|
65
|
+
return { alias: node, name: node.id.name };
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
node.type === "TSInterfaceDeclaration" ||
|
|
69
|
+
node.type === "TSEnumDeclaration" ||
|
|
70
|
+
node.type === "ClassDeclaration" ||
|
|
71
|
+
node.type === "ClassExpression"
|
|
72
|
+
) {
|
|
73
|
+
return node.id === null ? null : { alias: null, name: node.id.name };
|
|
74
|
+
}
|
|
75
|
+
if (
|
|
76
|
+
node.type === "ImportSpecifier" ||
|
|
77
|
+
node.type === "ImportDefaultSpecifier" ||
|
|
78
|
+
node.type === "ImportNamespaceSpecifier"
|
|
79
|
+
) {
|
|
80
|
+
return { alias: null, name: node.local.name };
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function collectTypeBindings(
|
|
86
|
+
node: ESTree.Node,
|
|
87
|
+
visitorKeys: VisitorKeys,
|
|
88
|
+
bindingsByName: Map<string, TypeBinding[]>,
|
|
89
|
+
aliases: ESTree.TSTypeAliasDeclaration[],
|
|
90
|
+
): void {
|
|
91
|
+
const declared = declaredTypeBinding(node);
|
|
92
|
+
if (declared !== null) {
|
|
93
|
+
const bindings = bindingsByName.get(declared.name) ?? [];
|
|
94
|
+
bindings.push({ ...declared, scope: enclosingTypeScope(node) });
|
|
95
|
+
bindingsByName.set(declared.name, bindings);
|
|
96
|
+
if (declared.alias !== null) aliases.push(declared.alias);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// SAFETY: Oxlint's visitor keys identify only ESTree child-node properties.
|
|
100
|
+
const fields = node as unknown as Readonly<Record<string, unknown>>;
|
|
101
|
+
for (const key of visitorKeys[node.type] ?? []) {
|
|
102
|
+
const value = fields[key];
|
|
103
|
+
if (isNode(value)) {
|
|
104
|
+
collectTypeBindings(value, visitorKeys, bindingsByName, aliases);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!Array.isArray(value)) continue;
|
|
108
|
+
for (const child of value) {
|
|
109
|
+
if (isNode(child)) {
|
|
110
|
+
collectTypeBindings(child, visitorKeys, bindingsByName, aliases);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Collect every lexical type alias and competing type binding in a program. */
|
|
117
|
+
export function createTypeAliasEnvironment(
|
|
118
|
+
program: ESTree.Program,
|
|
119
|
+
visitorKeys: VisitorKeys,
|
|
120
|
+
): TypeAliasEnvironment {
|
|
121
|
+
const cached = environmentsByProgram.get(program);
|
|
122
|
+
if (cached !== undefined) return cached;
|
|
123
|
+
const bindingsByName = new Map<string, TypeBinding[]>();
|
|
124
|
+
const aliases: ESTree.TSTypeAliasDeclaration[] = [];
|
|
125
|
+
collectTypeBindings(program, visitorKeys, bindingsByName, aliases);
|
|
126
|
+
const environment = { aliases, bindingsByName, visitorKeys };
|
|
127
|
+
environmentsByProgram.set(program, environment);
|
|
128
|
+
return environment;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function ancestorDistance(ancestor: ESTree.Node, node: ESTree.Node): number | null {
|
|
132
|
+
let current: ESTree.Node | null = node;
|
|
133
|
+
let distance = 0;
|
|
134
|
+
while (current !== null) {
|
|
135
|
+
if (current === ancestor) return distance;
|
|
136
|
+
current = current.parent;
|
|
137
|
+
distance += 1;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function nearestTypeBindings(
|
|
143
|
+
name: string,
|
|
144
|
+
use: ESTree.Node,
|
|
145
|
+
environment: TypeAliasEnvironment,
|
|
146
|
+
): readonly TypeBinding[] {
|
|
147
|
+
const candidates = environment.bindingsByName.get(name) ?? [];
|
|
148
|
+
let nearestDistance = Number.POSITIVE_INFINITY;
|
|
149
|
+
let nearest: TypeBinding[] = [];
|
|
150
|
+
for (const candidate of candidates) {
|
|
151
|
+
const distance = ancestorDistance(candidate.scope, use);
|
|
152
|
+
if (distance === null || distance > nearestDistance) continue;
|
|
153
|
+
if (distance === nearestDistance) {
|
|
154
|
+
nearest.push(candidate);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
nearestDistance = distance;
|
|
158
|
+
nearest = [candidate];
|
|
159
|
+
}
|
|
160
|
+
return nearest;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Resolve the nearest visible alias with this name, respecting lexical shadowing. */
|
|
164
|
+
export function visibleTypeAlias(
|
|
165
|
+
name: string,
|
|
166
|
+
use: ESTree.Node,
|
|
167
|
+
environment: TypeAliasEnvironment,
|
|
168
|
+
): ESTree.TSTypeAliasDeclaration | null {
|
|
169
|
+
if (lexicalTypeParameterNames(use, environment.visitorKeys).has(name)) return null;
|
|
170
|
+
const bindings = nearestTypeBindings(name, use, environment);
|
|
171
|
+
return bindings.length === 1 ? (bindings[0]?.alias ?? null) : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Return whether a local declaration shadows a built-in type at this use. */
|
|
175
|
+
export function hasVisibleTypeBinding(
|
|
176
|
+
name: string,
|
|
177
|
+
use: ESTree.Node,
|
|
178
|
+
environment: TypeAliasEnvironment,
|
|
179
|
+
): boolean {
|
|
180
|
+
return (
|
|
181
|
+
lexicalTypeParameterNames(use, environment.visitorKeys).has(name) ||
|
|
182
|
+
nearestTypeBindings(name, use, environment).length > 0
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function typeReferenceName(type: ESTree.TSTypeReference): string | null {
|
|
187
|
+
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function aliasSubstitutions(
|
|
191
|
+
alias: ESTree.TSTypeAliasDeclaration,
|
|
192
|
+
reference: ESTree.TSTypeReference,
|
|
193
|
+
base: Substitutions,
|
|
194
|
+
): Substitutions | null {
|
|
195
|
+
const parameters = alias.typeParameters?.params ?? [];
|
|
196
|
+
const arguments_ = reference.typeArguments?.params ?? [];
|
|
197
|
+
const next = new Map(base);
|
|
198
|
+
for (const [index, parameter] of parameters.entries()) {
|
|
199
|
+
const explicitArgument = arguments_[index];
|
|
200
|
+
const argument = explicitArgument ?? parameter.default;
|
|
201
|
+
if (argument === null || argument === undefined) return null;
|
|
202
|
+
const argumentSubstitutions = explicitArgument === undefined ? next : base;
|
|
203
|
+
next.set(parameter.name.name, {
|
|
204
|
+
type: argument,
|
|
205
|
+
substitutions: new Map(argumentSubstitutions),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return next;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Match a type after resolving visible aliases and substituting their type parameters. */
|
|
212
|
+
export function resolvedTypeMatches(
|
|
213
|
+
type: ESTree.TSType,
|
|
214
|
+
environment: TypeAliasEnvironment,
|
|
215
|
+
matcher: ResolvedTypeMatcher,
|
|
216
|
+
): boolean {
|
|
217
|
+
const evaluate = (
|
|
218
|
+
current: ESTree.TSType,
|
|
219
|
+
substitutions: Substitutions,
|
|
220
|
+
resolvingAliases: ReadonlySet<ESTree.TSTypeAliasDeclaration>,
|
|
221
|
+
): boolean => {
|
|
222
|
+
if (current.type === "TSTypeReference") {
|
|
223
|
+
const name = typeReferenceName(current);
|
|
224
|
+
if (name !== null) {
|
|
225
|
+
const substitution = substitutions.get(name);
|
|
226
|
+
if (substitution !== undefined && !current.typeArguments?.params.length) {
|
|
227
|
+
return evaluate(
|
|
228
|
+
substitution.type,
|
|
229
|
+
substitution.substitutions,
|
|
230
|
+
resolvingAliases,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
const alias = visibleTypeAlias(name, current, environment);
|
|
234
|
+
if (alias !== null && !resolvingAliases.has(alias)) {
|
|
235
|
+
const nextSubstitutions = aliasSubstitutions(alias, current, substitutions);
|
|
236
|
+
if (nextSubstitutions !== null) {
|
|
237
|
+
const nextResolving = new Set(resolvingAliases);
|
|
238
|
+
nextResolving.add(alias);
|
|
239
|
+
return evaluate(alias.typeAnnotation, nextSubstitutions, nextResolving);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return matcher(current, (child) =>
|
|
245
|
+
evaluate(child, substitutions, resolvingAliases),
|
|
246
|
+
);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
return evaluate(type, new Map(), new Set());
|
|
250
|
+
}
|