ai-deslop 0.5.3 → 0.6.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/dist/rules/max-boolean-conditions.js +18 -1
- package/dist/rules/no-call-only-assertions.js +22 -1
- package/dist/rules/no-duplicate-consts.js +9 -3
- package/dist/rules/no-duplicate-types.js +25 -12
- package/dist/rules/no-pass-through-schema.js +18 -6
- package/dist/rules/no-pass-through-type-alias.js +14 -2
- package/dist/rules/no-poor-substring-tests.js +57 -22
- package/dist/rules/no-reexports-outside-entrypoints.js +18 -7
- package/dist/rules/no-renamed-imports.js +81 -4
- package/dist/vendor/anti-slop/rules/no-known-value-widening.js +8 -1
- package/dist/vendor/anti-slop/rules/no-module-mocking.js +4 -3
- package/dist/vendor/anti-slop/rules/no-object-parameters.js +1 -1
- package/dist/vendor/anti-slop/rules/no-reduce-accumulator-copy.js +6 -1
- package/dist/vendor/anti-slop/rules/no-reflect-apply.js +2 -0
- package/dist/vendor/anti-slop/rules/no-reflect-get.js +2 -0
- package/dist/vendor/anti-slop/rules/no-runtime-typeof.js +2 -0
- package/dist/vendor/anti-slop/rules/no-shape-in-symbol-names.js +3 -0
- package/dist/vendor/anti-slop/rules/no-unknown-returns.js +1 -1
- package/dist/vendor/anti-slop/rules/no-unknown-type-aliases.js +1 -1
- package/dist/vendor/anti-slop/rules/no-unsafe-dictionary-type.js +1 -1
- package/dist/vendor/anti-slop/rules/no-widen-then-assert.js +4 -2
- package/dist/vendor/anti-slop/shared/dictionary-types.d.ts +1 -1
- package/dist/vendor/anti-slop/shared/dictionary-types.js +2 -2
- package/dist/vendor/anti-slop/shared/type-alias-resolution.d.ts +2 -2
- package/dist/vendor/anti-slop/shared/type-alias-resolution.js +56 -13
- package/package.json +1 -1
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
function countRawTerms(node) {
|
|
3
|
+
const pending = [node];
|
|
4
|
+
let count = 0;
|
|
5
|
+
while (pending.length > 0) {
|
|
6
|
+
const term = pending.pop();
|
|
7
|
+
if (term.type === "LogicalExpression" && term.operator !== "??") {
|
|
8
|
+
pending.push(term.left, term.right);
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
count++;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return count;
|
|
15
|
+
}
|
|
2
16
|
/** Limit top-level Boolean terms in control-flow conditions. */
|
|
3
17
|
export const maxBooleanConditionsRule = defineRule({
|
|
4
18
|
meta: {
|
|
@@ -21,6 +35,10 @@ export const maxBooleanConditionsRule = defineRule({
|
|
|
21
35
|
function checkCondition(node) {
|
|
22
36
|
if (node.test === null)
|
|
23
37
|
return;
|
|
38
|
+
// Grouping can only merge terms, so skip token reads when the ungrouped count fits.
|
|
39
|
+
const max = options.maxConditions ?? 3;
|
|
40
|
+
if (countRawTerms(node.test) <= max)
|
|
41
|
+
return;
|
|
24
42
|
const pending = [node.test];
|
|
25
43
|
let count = 0;
|
|
26
44
|
while (pending.length > 0) {
|
|
@@ -35,7 +53,6 @@ export const maxBooleanConditionsRule = defineRule({
|
|
|
35
53
|
count++;
|
|
36
54
|
}
|
|
37
55
|
}
|
|
38
|
-
const max = options.maxConditions ?? 3;
|
|
39
56
|
if (count <= max)
|
|
40
57
|
return;
|
|
41
58
|
if (options.message !== undefined) {
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
2
|
// Tests whose every assertion is a "was it called" mock check verify orchestration, not behavior.
|
|
3
3
|
// `toHaveBeenCalledWith` is allowed because it asserts on real data.
|
|
4
|
-
const CALL_ONLY_MATCHERS = new Set([
|
|
4
|
+
const CALL_ONLY_MATCHERS = new Set([
|
|
5
|
+
"toHaveBeenCalled",
|
|
6
|
+
"toHaveBeenCalledTimes",
|
|
7
|
+
"toHaveBeenCalledOnce",
|
|
8
|
+
"toBeCalled",
|
|
9
|
+
"toBeCalledTimes",
|
|
10
|
+
]);
|
|
5
11
|
const TEST_NAMES = new Set(["it", "test"]);
|
|
6
12
|
const MODIFIERS = new Set(["not", "resolves", "rejects"]);
|
|
7
13
|
function rootIdentifierName(node) {
|
|
@@ -42,6 +48,14 @@ function matcherName(node) {
|
|
|
42
48
|
const isExpectCall = object.type === "CallExpression" && rootIdentifierName(object.callee) === "expect";
|
|
43
49
|
return isExpectCall ? node.callee.property.name : undefined;
|
|
44
50
|
}
|
|
51
|
+
// `assert(...)`, `assert.equal(...)`, `assert.strict.equal(...)`: real assertions.
|
|
52
|
+
function isAssertCall(node) {
|
|
53
|
+
let callee = node.callee;
|
|
54
|
+
while (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier") {
|
|
55
|
+
callee = callee.object;
|
|
56
|
+
}
|
|
57
|
+
return callee.type === "Identifier" && callee.name === "assert";
|
|
58
|
+
}
|
|
45
59
|
/** Tests must assert on behavior, not only that a mock was called. */
|
|
46
60
|
export const noCallOnlyAssertionsRule = defineRule({
|
|
47
61
|
meta: {
|
|
@@ -57,6 +71,9 @@ export const noCallOnlyAssertionsRule = defineRule({
|
|
|
57
71
|
return {
|
|
58
72
|
before() {
|
|
59
73
|
tests = [];
|
|
74
|
+
// Every call-only matcher name contains "Called". `\u` covers escaped identifiers.
|
|
75
|
+
const text = context.sourceCode.text;
|
|
76
|
+
return text.includes("Called") || text.includes("\\u");
|
|
60
77
|
},
|
|
61
78
|
CallExpression(node) {
|
|
62
79
|
if (isTestCall(node)) {
|
|
@@ -66,6 +83,10 @@ export const noCallOnlyAssertionsRule = defineRule({
|
|
|
66
83
|
const test = tests.at(-1);
|
|
67
84
|
if (!test)
|
|
68
85
|
return;
|
|
86
|
+
if (isAssertCall(node)) {
|
|
87
|
+
test.total += 1;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
69
90
|
const matcher = matcherName(node);
|
|
70
91
|
if (!matcher)
|
|
71
92
|
return;
|
|
@@ -38,12 +38,13 @@ export const noDuplicateConstsRule = defineRule({
|
|
|
38
38
|
},
|
|
39
39
|
createOnce(context) {
|
|
40
40
|
const seen = new Map();
|
|
41
|
+
const seenKeysByFile = new Map();
|
|
41
42
|
return {
|
|
42
43
|
Program() {
|
|
43
44
|
// LSPs can re-lint a file in the same worker; discard stale entries first.
|
|
44
|
-
for (const
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
for (const key of seenKeysByFile.get(context.filename) ?? [])
|
|
46
|
+
seen.delete(key);
|
|
47
|
+
seenKeysByFile.delete(context.filename);
|
|
47
48
|
},
|
|
48
49
|
VariableDeclaration(node) {
|
|
49
50
|
if (node.kind !== "const")
|
|
@@ -57,6 +58,11 @@ export const noDuplicateConstsRule = defineRule({
|
|
|
57
58
|
const key = `${declaration.id.name}:${value}`, first = seen.get(key);
|
|
58
59
|
if (first === undefined) {
|
|
59
60
|
seen.set(key, { filename: context.filename, name: declaration.id.name });
|
|
61
|
+
const keys = seenKeysByFile.get(context.filename);
|
|
62
|
+
if (keys === undefined)
|
|
63
|
+
seenKeysByFile.set(context.filename, [key]);
|
|
64
|
+
else
|
|
65
|
+
keys.push(key);
|
|
60
66
|
}
|
|
61
67
|
else if (first.filename !== context.filename) {
|
|
62
68
|
context.report({ node: declaration, messageId: "duplicate", data: { name: declaration.id.name, firstFile: first.filename } });
|
|
@@ -16,7 +16,7 @@ export const noDuplicateTypesRule = defineRule({
|
|
|
16
16
|
schema: [{ type: "object", properties: { message: { type: "string" }, minProperties: { type: "integer", minimum: 0 }, schemas: { type: "object", properties: { libraries: { type: "array", items: { enum: ["zod", "valibot", "arktype"] } }, matchTypes: { type: "boolean" } }, additionalProperties: false }, drizzle: { type: "object", properties: { models: { type: "array", items: { enum: ["select", "insert"] } }, matchTypes: { type: "boolean" } }, additionalProperties: false } }, additionalProperties: false }],
|
|
17
17
|
},
|
|
18
18
|
createOnce(context) {
|
|
19
|
-
const seen = new Map();
|
|
19
|
+
const seen = new Map(), seenKeysByFile = new Map();
|
|
20
20
|
const zodNamespaces = new Set(), valibotNamespaces = new Set(), arkTypeFunctions = new Set();
|
|
21
21
|
const valibotFunctions = new Map(), drizzleTables = new Set(), drizzleSchemaFactories = new Set(), drizzleSchemaObjects = new Set();
|
|
22
22
|
const drizzleBuilders = new Map(), drizzleModels = new Map();
|
|
@@ -29,10 +29,16 @@ export const noDuplicateTypesRule = defineRule({
|
|
|
29
29
|
const isolates = kind === "schema" ? options.schemas?.matchTypes === false : kind.startsWith("drizzle-") && options.drizzle?.matchTypes === false;
|
|
30
30
|
const key = isolates ? `${kind}:${fingerprint}` : fingerprint, first = seen.get(key);
|
|
31
31
|
if (first === undefined) {
|
|
32
|
-
seen.set(key, { node, name, filename: context.filename });
|
|
32
|
+
seen.set(key, { start: node.start, name, filename: context.filename });
|
|
33
|
+
const keys = seenKeysByFile.get(context.filename);
|
|
34
|
+
if (keys === undefined)
|
|
35
|
+
seenKeysByFile.set(context.filename, [key]);
|
|
36
|
+
else
|
|
37
|
+
keys.push(key);
|
|
33
38
|
return;
|
|
34
39
|
}
|
|
35
|
-
|
|
40
|
+
// The same declarator can produce one fingerprint for several Drizzle models.
|
|
41
|
+
if (first.filename !== context.filename || first.start !== node.start)
|
|
36
42
|
context.report({ node, messageId: "duplicate", data: { message: options.message ?? "This type is structurally identical to an existing declaration.", firstName: first.name, firstFile: first.filename } });
|
|
37
43
|
};
|
|
38
44
|
const inspectType = (node) => {
|
|
@@ -42,7 +48,10 @@ export const noDuplicateTypesRule = defineRule({
|
|
|
42
48
|
report(node, node.id.name, fingerprint, "type");
|
|
43
49
|
};
|
|
44
50
|
const inspectSchema = (node) => {
|
|
45
|
-
|
|
51
|
+
// Every schema form is a call, and needs a library binding imported earlier in the file.
|
|
52
|
+
if (node.id.type !== "Identifier" || node.init?.type !== "CallExpression")
|
|
53
|
+
return;
|
|
54
|
+
if (zodNamespaces.size === 0 && valibotNamespaces.size === 0 && valibotFunctions.size === 0 && !hasArkTypeImport)
|
|
46
55
|
return;
|
|
47
56
|
const zod = memberCall(node.init);
|
|
48
57
|
const valibot = factoryCall(node.init, valibotNamespaces, valibotFunctions);
|
|
@@ -55,7 +64,7 @@ export const noDuplicateTypesRule = defineRule({
|
|
|
55
64
|
report(node, node.id.name, schema.fingerprint, "schema");
|
|
56
65
|
};
|
|
57
66
|
const inspectDrizzle = (node) => {
|
|
58
|
-
if (options.drizzle === undefined || node.id.type !== "Identifier" || node.init ===
|
|
67
|
+
if (options.drizzle === undefined || node.id.type !== "Identifier" || node.init?.type !== "CallExpression" || drizzleTables.size === 0 && drizzleSchemaObjects.size === 0)
|
|
59
68
|
return;
|
|
60
69
|
const models = new Map();
|
|
61
70
|
for (const model of options.drizzle.models ?? ["select", "insert"]) {
|
|
@@ -69,18 +78,19 @@ export const noDuplicateTypesRule = defineRule({
|
|
|
69
78
|
drizzleModels.set(node.id.name, models);
|
|
70
79
|
};
|
|
71
80
|
const trackImport = (node) => {
|
|
72
|
-
|
|
81
|
+
// Only track bindings for enabled detectors; the inspectors read nothing else.
|
|
82
|
+
if (node.source.value === "zod" && enabled("zod"))
|
|
73
83
|
for (const specifier of node.specifiers)
|
|
74
84
|
if (specifier.type === "ImportNamespaceSpecifier" || specifier.type === "ImportDefaultSpecifier" || specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier" && specifier.imported.name === "z")
|
|
75
85
|
zodNamespaces.add(specifier.local.name);
|
|
76
|
-
if (node.source.value === "valibot")
|
|
86
|
+
if (node.source.value === "valibot" && enabled("valibot"))
|
|
77
87
|
for (const specifier of node.specifiers) {
|
|
78
88
|
if (specifier.type === "ImportNamespaceSpecifier")
|
|
79
89
|
valibotNamespaces.add(specifier.local.name);
|
|
80
90
|
if (specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier")
|
|
81
91
|
valibotFunctions.set(specifier.local.name, specifier.imported.name);
|
|
82
92
|
}
|
|
83
|
-
if (node.source.value === "arktype") {
|
|
93
|
+
if (node.source.value === "arktype" && enabled("arktype")) {
|
|
84
94
|
hasArkTypeImport = true;
|
|
85
95
|
for (const specifier of node.specifiers) {
|
|
86
96
|
const imported = specifier.type === "ImportSpecifier" ? specifier.imported : null;
|
|
@@ -88,6 +98,8 @@ export const noDuplicateTypesRule = defineRule({
|
|
|
88
98
|
arkTypeFunctions.add(specifier.local.name);
|
|
89
99
|
}
|
|
90
100
|
}
|
|
101
|
+
if (options.drizzle === undefined)
|
|
102
|
+
return;
|
|
91
103
|
const source = /^drizzle-orm\/(pg|mysql|sqlite)-core$/.exec(String(node.source.value));
|
|
92
104
|
if (source === null)
|
|
93
105
|
return;
|
|
@@ -107,9 +119,9 @@ export const noDuplicateTypesRule = defineRule({
|
|
|
107
119
|
return {
|
|
108
120
|
Program() {
|
|
109
121
|
// LSPs can re-lint a file in the same createOnce worker; discard its stale declarations first.
|
|
110
|
-
for (const
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
for (const key of seenKeysByFile.get(context.filename) ?? [])
|
|
123
|
+
seen.delete(key);
|
|
124
|
+
seenKeysByFile.delete(context.filename);
|
|
113
125
|
options = context.options[0] ?? {};
|
|
114
126
|
zodNamespaces.clear();
|
|
115
127
|
valibotNamespaces.clear();
|
|
@@ -124,7 +136,8 @@ export const noDuplicateTypesRule = defineRule({
|
|
|
124
136
|
hasArkTypeImport = false;
|
|
125
137
|
},
|
|
126
138
|
ImportDeclaration: trackImport,
|
|
127
|
-
TSTypeAliasDeclaration(node) { inspectType(node);
|
|
139
|
+
TSTypeAliasDeclaration(node) { inspectType(node); if (options.drizzle !== undefined)
|
|
140
|
+
drizzleAliases.push(node); },
|
|
128
141
|
TSInterfaceDeclaration: inspectType,
|
|
129
142
|
VariableDeclarator(node) {
|
|
130
143
|
inspectSchema(node);
|
|
@@ -67,7 +67,9 @@ export const noPassThroughSchemaRule = defineRule({
|
|
|
67
67
|
createOnce(context) {
|
|
68
68
|
let options;
|
|
69
69
|
const candidates = [];
|
|
70
|
-
|
|
70
|
+
// Export identifiers are resolved to variables only when an alias needs the check.
|
|
71
|
+
const exportedNames = [];
|
|
72
|
+
let exported = null;
|
|
71
73
|
function variableFor(node) {
|
|
72
74
|
let scope = context.sourceCode.getScope(node);
|
|
73
75
|
while (true) {
|
|
@@ -150,15 +152,25 @@ export const noPassThroughSchemaRule = defineRule({
|
|
|
150
152
|
return null;
|
|
151
153
|
}
|
|
152
154
|
function markExport(node) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
exportedNames.push(node);
|
|
156
|
+
}
|
|
157
|
+
function exportedVariables() {
|
|
158
|
+
if (exported === null) {
|
|
159
|
+
exported = new Set();
|
|
160
|
+
for (const node of exportedNames) {
|
|
161
|
+
const variable = variableFor(node);
|
|
162
|
+
if (variable !== null)
|
|
163
|
+
exported.add(variable);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return exported;
|
|
156
167
|
}
|
|
157
168
|
return {
|
|
158
169
|
before() {
|
|
159
170
|
options = context.options[0] ?? {};
|
|
160
171
|
candidates.length = 0;
|
|
161
|
-
|
|
172
|
+
exportedNames.length = 0;
|
|
173
|
+
exported = null;
|
|
162
174
|
},
|
|
163
175
|
VariableDeclarator(node) {
|
|
164
176
|
if (node.parent.type === "VariableDeclaration" && node.parent.kind === "const" && node.id.type === "Identifier" && node.init?.type === "Identifier")
|
|
@@ -190,7 +202,7 @@ export const noPassThroughSchemaRule = defineRule({
|
|
|
190
202
|
if (target !== "schema" && target !== "zod-schema" && target !== "ark-schema")
|
|
191
203
|
continue;
|
|
192
204
|
const variable = variableFor(node.id);
|
|
193
|
-
if (options.allowExportedAliases && variable !== null &&
|
|
205
|
+
if (options.allowExportedAliases && variable !== null && exportedVariables().has(variable))
|
|
194
206
|
continue;
|
|
195
207
|
context.report({ node, messageId: "passThroughSchema", data: { schemaName: node.init.name } });
|
|
196
208
|
}
|
|
@@ -5,16 +5,28 @@ function isForwardedTypeParameter(typeArgument, typeParameter) {
|
|
|
5
5
|
typeArgument.typeName.name === typeParameter.name.name &&
|
|
6
6
|
!typeArgument.typeArguments);
|
|
7
7
|
}
|
|
8
|
+
function typeNameText(node) {
|
|
9
|
+
if (node.type === "Identifier")
|
|
10
|
+
return node.name;
|
|
11
|
+
if (node.type !== "TSQualifiedName")
|
|
12
|
+
return null;
|
|
13
|
+
const left = typeNameText(node.left);
|
|
14
|
+
return left === null ? null : `${left}.${node.right.name}`;
|
|
15
|
+
}
|
|
8
16
|
function passThroughTargetName(node) {
|
|
9
17
|
const annotation = node.typeAnnotation;
|
|
10
|
-
if (annotation.type !== "TSTypeReference"
|
|
18
|
+
if (annotation.type !== "TSTypeReference") {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const typeName = typeNameText(annotation.typeName);
|
|
22
|
+
if (typeName === null) {
|
|
11
23
|
return null;
|
|
12
24
|
}
|
|
13
25
|
const typeParameters = node.typeParameters?.params ?? [];
|
|
14
26
|
const typeArguments = annotation.typeArguments?.params ?? [];
|
|
15
27
|
const isPassThrough = typeParameters.length === typeArguments.length &&
|
|
16
28
|
typeParameters.every((typeParameter, index) => isForwardedTypeParameter(typeArguments[index], typeParameter));
|
|
17
|
-
return isPassThrough ?
|
|
29
|
+
return isPassThrough ? typeName : null;
|
|
18
30
|
}
|
|
19
31
|
/** Disallow type aliases that only rename another type. */
|
|
20
32
|
export const noPassThroughTypeAliasRule = defineRule({
|
|
@@ -42,7 +42,40 @@ const ASSERT_MATCHERS = new Set([
|
|
|
42
42
|
"match",
|
|
43
43
|
"notMatch",
|
|
44
44
|
]);
|
|
45
|
+
// Bounded, insertion-ordered cache. Entries are validated by mtime and size so that
|
|
46
|
+
// long-lived processes (editor/LSP) see on-disk changes.
|
|
47
|
+
const FILE_CONTENT_CACHE_LIMIT = 256;
|
|
45
48
|
const fileContentCache = new Map();
|
|
49
|
+
// TS ESM style: a `.js` specifier may refer to a `.ts` source file, etc.
|
|
50
|
+
const SOURCE_EXTENSIONS_FOR_SPECIFIER = {
|
|
51
|
+
".js": [".ts", ".tsx"],
|
|
52
|
+
".jsx": [".tsx"],
|
|
53
|
+
".mjs": [".mts"],
|
|
54
|
+
".cjs": [".cts"],
|
|
55
|
+
};
|
|
56
|
+
function readCachedFile(filePath) {
|
|
57
|
+
const stats = fs.statSync(filePath, { throwIfNoEntry: false });
|
|
58
|
+
if (!stats?.isFile()) {
|
|
59
|
+
fileContentCache.delete(filePath);
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const cached = fileContentCache.get(filePath);
|
|
63
|
+
if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
|
|
64
|
+
return cached.content;
|
|
65
|
+
}
|
|
66
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
67
|
+
fileContentCache.delete(filePath);
|
|
68
|
+
if (fileContentCache.size >= FILE_CONTENT_CACHE_LIMIT) {
|
|
69
|
+
const oldest = fileContentCache.keys().next().value;
|
|
70
|
+
if (oldest !== undefined)
|
|
71
|
+
fileContentCache.delete(oldest);
|
|
72
|
+
}
|
|
73
|
+
fileContentCache.set(filePath, { mtimeMs: stats.mtimeMs, size: stats.size, content });
|
|
74
|
+
return content;
|
|
75
|
+
}
|
|
76
|
+
function escapeRegExp(text) {
|
|
77
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
78
|
+
}
|
|
46
79
|
function isTestFile(filename) {
|
|
47
80
|
if (!filename)
|
|
48
81
|
return false;
|
|
@@ -98,36 +131,37 @@ function isImportedConstantString(variable, currentFilename) {
|
|
|
98
131
|
if (typeof source !== "string" || !source.startsWith(".")) {
|
|
99
132
|
return false;
|
|
100
133
|
}
|
|
134
|
+
const importedName = definition.node.type === "ImportSpecifier"
|
|
135
|
+
? definition.node.imported.type === "Identifier"
|
|
136
|
+
? definition.node.imported.name
|
|
137
|
+
: definition.node.imported.value
|
|
138
|
+
: definition.node.type === "ImportDefaultSpecifier"
|
|
139
|
+
? "default"
|
|
140
|
+
: null;
|
|
141
|
+
if (!importedName)
|
|
142
|
+
return false;
|
|
101
143
|
const dir = path.dirname(currentFilename);
|
|
144
|
+
const specifierExtension = path.extname(source);
|
|
145
|
+
const stem = source.slice(0, source.length - specifierExtension.length);
|
|
102
146
|
const candidates = [
|
|
103
147
|
path.resolve(dir, source),
|
|
148
|
+
...(SOURCE_EXTENSIONS_FOR_SPECIFIER[specifierExtension] ?? []).map((extension) => path.resolve(dir, `${stem}${extension}`)),
|
|
104
149
|
path.resolve(dir, `${source}.ts`),
|
|
105
150
|
path.resolve(dir, `${source}.tsx`),
|
|
151
|
+
path.resolve(dir, `${source}.mts`),
|
|
152
|
+
path.resolve(dir, `${source}.cts`),
|
|
106
153
|
path.resolve(dir, `${source}.js`),
|
|
154
|
+
path.resolve(dir, `${source}.jsx`),
|
|
107
155
|
path.resolve(dir, `${source}.mjs`),
|
|
108
156
|
path.resolve(dir, source, "index.ts"),
|
|
109
157
|
path.resolve(dir, source, "index.js"),
|
|
110
158
|
];
|
|
159
|
+
const pattern = new RegExp(`export\\s+const\\s+${escapeRegExp(importedName)}\\s*=\\s*(["'\`])`);
|
|
111
160
|
for (const candidate of candidates) {
|
|
112
161
|
try {
|
|
113
|
-
|
|
114
|
-
if (content === undefined)
|
|
115
|
-
if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) {
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
content = fs.readFileSync(candidate, "utf8");
|
|
119
|
-
fileContentCache.set(candidate, content);
|
|
120
|
-
}
|
|
121
|
-
const importedName = definition.node.type === "ImportSpecifier"
|
|
122
|
-
? definition.node.imported.type === "Identifier"
|
|
123
|
-
? definition.node.imported.name
|
|
124
|
-
: definition.node.imported.value
|
|
125
|
-
: definition.node.type === "ImportDefaultSpecifier"
|
|
126
|
-
? "default"
|
|
127
|
-
: null;
|
|
128
|
-
if (!importedName)
|
|
162
|
+
const content = readCachedFile(candidate);
|
|
163
|
+
if (content === undefined)
|
|
129
164
|
continue;
|
|
130
|
-
const pattern = new RegExp(`export\\s+const\\s+${importedName}\\s*=\\s*(["'\`])`);
|
|
131
165
|
if (pattern.test(content)) {
|
|
132
166
|
return true;
|
|
133
167
|
}
|
|
@@ -249,14 +283,15 @@ export const noPoorSubstringTestsRule = defineRule({
|
|
|
249
283
|
schema: [],
|
|
250
284
|
},
|
|
251
285
|
createOnce(context) {
|
|
252
|
-
let shouldCheck = false;
|
|
253
286
|
return {
|
|
254
|
-
|
|
255
|
-
|
|
287
|
+
before() {
|
|
288
|
+
if (!isTestFile(context.filename))
|
|
289
|
+
return false;
|
|
290
|
+
// Every pattern needs an `expect` or `assert` callee. `\u` covers escaped identifiers.
|
|
291
|
+
const text = context.sourceCode.text;
|
|
292
|
+
return text.includes("expect") || text.includes("assert") || text.includes("\\u");
|
|
256
293
|
},
|
|
257
294
|
CallExpression(node) {
|
|
258
|
-
if (!shouldCheck)
|
|
259
|
-
return;
|
|
260
295
|
// Pattern 1: expect(subject).toContain(sub), expect(subject).toMatch(regex), etc.
|
|
261
296
|
if (node.callee.type === "MemberExpression" &&
|
|
262
297
|
!node.callee.computed &&
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { matchesGlob, relative } from "node:path";
|
|
2
2
|
import { defineRule } from "@oxlint/plugins";
|
|
3
3
|
import { resolveVariable } from "../vendor/anti-slop/shared/scope.js";
|
|
4
|
+
const defaultEntrypoints = ["**/index.ts", "**/index.tsx", "**/index.mts", "**/index.cts", "**/index.js", "**/index.jsx", "**/index.mjs"];
|
|
4
5
|
/** Keep re-exports in explicitly allowed entrypoint files. */
|
|
5
6
|
export const noReexportsOutsideEntrypointsRule = defineRule({
|
|
6
7
|
meta: {
|
|
@@ -21,28 +22,38 @@ export const noReexportsOutsideEntrypointsRule = defineRule({
|
|
|
21
22
|
function isImported(node) {
|
|
22
23
|
return resolveVariable(context.sourceCode, node)?.defs.some((definition) => definition.type === "ImportBinding") ?? false;
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
// Most files have no re-exports, so match the entrypoint globs only on the first one.
|
|
26
|
+
let entrypoint;
|
|
27
|
+
function isEntrypoint() {
|
|
28
|
+
if (entrypoint === undefined) {
|
|
26
29
|
const options = context.options[0];
|
|
27
30
|
const filename = relative(context.cwd, context.filename).replaceAll("\\", "/");
|
|
28
|
-
|
|
31
|
+
entrypoint = (options?.entrypoints ?? defaultEntrypoints).some((pattern) => matchesGlob(filename, pattern));
|
|
32
|
+
}
|
|
33
|
+
return entrypoint;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
before() {
|
|
37
|
+
entrypoint = undefined;
|
|
29
38
|
},
|
|
30
39
|
ExportNamedDeclaration(node) {
|
|
31
40
|
if (node.source !== null) {
|
|
32
|
-
|
|
41
|
+
if (!isEntrypoint())
|
|
42
|
+
context.report({ node, messageId: "reexport" });
|
|
33
43
|
return;
|
|
34
44
|
}
|
|
35
45
|
for (const specifier of node.specifiers) {
|
|
36
|
-
if (specifier.local.type === "Identifier" && isImported(specifier.local)) {
|
|
46
|
+
if (specifier.local.type === "Identifier" && isImported(specifier.local) && !isEntrypoint()) {
|
|
37
47
|
context.report({ node: specifier, messageId: "reexport" });
|
|
38
48
|
}
|
|
39
49
|
}
|
|
40
50
|
},
|
|
41
51
|
ExportAllDeclaration(node) {
|
|
42
|
-
|
|
52
|
+
if (!isEntrypoint())
|
|
53
|
+
context.report({ node, messageId: "reexport" });
|
|
43
54
|
},
|
|
44
55
|
ExportDefaultDeclaration(node) {
|
|
45
|
-
if (node.declaration.type === "Identifier" && isImported(node.declaration)) {
|
|
56
|
+
if (node.declaration.type === "Identifier" && isImported(node.declaration) && !isEntrypoint()) {
|
|
46
57
|
context.report({ node, messageId: "reexport" });
|
|
47
58
|
}
|
|
48
59
|
},
|
|
@@ -11,6 +11,78 @@ function moduleScopeFor(scope) {
|
|
|
11
11
|
function importedName(node) {
|
|
12
12
|
return node.imported.type === "Identifier" ? node.imported.name : null;
|
|
13
13
|
}
|
|
14
|
+
const reservedBindingNames = new Set([
|
|
15
|
+
"arguments",
|
|
16
|
+
"await",
|
|
17
|
+
"break",
|
|
18
|
+
"case",
|
|
19
|
+
"catch",
|
|
20
|
+
"class",
|
|
21
|
+
"const",
|
|
22
|
+
"continue",
|
|
23
|
+
"debugger",
|
|
24
|
+
"default",
|
|
25
|
+
"delete",
|
|
26
|
+
"do",
|
|
27
|
+
"else",
|
|
28
|
+
"enum",
|
|
29
|
+
"eval",
|
|
30
|
+
"export",
|
|
31
|
+
"extends",
|
|
32
|
+
"false",
|
|
33
|
+
"finally",
|
|
34
|
+
"for",
|
|
35
|
+
"function",
|
|
36
|
+
"if",
|
|
37
|
+
"implements",
|
|
38
|
+
"import",
|
|
39
|
+
"in",
|
|
40
|
+
"instanceof",
|
|
41
|
+
"interface",
|
|
42
|
+
"let",
|
|
43
|
+
"new",
|
|
44
|
+
"null",
|
|
45
|
+
"package",
|
|
46
|
+
"private",
|
|
47
|
+
"protected",
|
|
48
|
+
"public",
|
|
49
|
+
"return",
|
|
50
|
+
"static",
|
|
51
|
+
"super",
|
|
52
|
+
"switch",
|
|
53
|
+
"this",
|
|
54
|
+
"throw",
|
|
55
|
+
"true",
|
|
56
|
+
"try",
|
|
57
|
+
"typeof",
|
|
58
|
+
"var",
|
|
59
|
+
"void",
|
|
60
|
+
"while",
|
|
61
|
+
"with",
|
|
62
|
+
"yield",
|
|
63
|
+
]);
|
|
64
|
+
function isShadowedAt(reference, name, moduleScope) {
|
|
65
|
+
let current = reference.from;
|
|
66
|
+
while (current !== null && current !== moduleScope) {
|
|
67
|
+
if (current.set.has(name))
|
|
68
|
+
return true;
|
|
69
|
+
current = current.upper;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
function isIntrinsicJsxTag(identifier, name) {
|
|
74
|
+
const parent = identifier.parent;
|
|
75
|
+
return (parent.type === "JSXOpeningElement" || parent.type === "JSXClosingElement") && /^[a-z]/.test(name);
|
|
76
|
+
}
|
|
77
|
+
function referenceReplacement(identifier, localName, name) {
|
|
78
|
+
const parent = identifier.parent;
|
|
79
|
+
if (parent.type === "Property" && parent.shorthand && parent.value === identifier)
|
|
80
|
+
return `${localName}: ${name}`;
|
|
81
|
+
if (parent.type === "ExportSpecifier" && parent.local === identifier && parent.exported.range[0] === identifier.range[0]) {
|
|
82
|
+
return `${name} as ${localName}`;
|
|
83
|
+
}
|
|
84
|
+
return name;
|
|
85
|
+
}
|
|
14
86
|
/** Disallow named import aliases when the imported name is available in module scope. */
|
|
15
87
|
export const noRenamedImportsRule = defineRule({
|
|
16
88
|
meta: {
|
|
@@ -39,11 +111,16 @@ export const noRenamedImportsRule = defineRule({
|
|
|
39
111
|
messageId: "unnecessaryAlias",
|
|
40
112
|
data: { importedName: name, localName: node.local.name },
|
|
41
113
|
fix(fixer) {
|
|
114
|
+
const localName = node.local.name;
|
|
115
|
+
const references = variable.references.filter((reference) => reference.resolved === variable);
|
|
116
|
+
if (reservedBindingNames.has(name) ||
|
|
117
|
+
moduleScope.through.some((reference) => reference.identifier.name === name) ||
|
|
118
|
+
references.some((reference) => isShadowedAt(reference, name, moduleScope) || isIntrinsicJsxTag(reference.identifier, name))) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
42
121
|
return [
|
|
43
|
-
fixer.
|
|
44
|
-
...
|
|
45
|
-
.filter((reference) => reference.resolved === variable)
|
|
46
|
-
.map((reference) => fixer.replaceText(reference.identifier, name)),
|
|
122
|
+
fixer.replaceTextRange([node.imported.range[0], node.range[1]], name),
|
|
123
|
+
...references.map((reference) => fixer.replaceText(reference.identifier, referenceReplacement(reference.identifier, localName, name))),
|
|
47
124
|
];
|
|
48
125
|
},
|
|
49
126
|
});
|
|
@@ -2,6 +2,7 @@ import { defineRule } from "@oxlint/plugins";
|
|
|
2
2
|
import { classifyUnsafeDictionaryValue, classifyWideningTarget, createTypeEnvironment, isKnownEvidenceExpression, } from "../shared/dictionary-types.js";
|
|
3
3
|
import { containsUnknownType, functionParameterBindingName, functionParameterTypeAnnotation, } from "../shared/function-parameters.js";
|
|
4
4
|
import { resolveVariable } from "../shared/scope.js";
|
|
5
|
+
const TYPE_PREDICATE_KEYWORD = /\b(?:asserts|is)\b/;
|
|
5
6
|
function unwrapExpression(expression) {
|
|
6
7
|
let current = expression;
|
|
7
8
|
while (current.type === "ParenthesizedExpression" ||
|
|
@@ -196,6 +197,7 @@ export const noKnownValueWideningRule = defineRule({
|
|
|
196
197
|
},
|
|
197
198
|
createOnce(context) {
|
|
198
199
|
let environment = null;
|
|
200
|
+
let hasTypePredicateKeyword = null;
|
|
199
201
|
const reportFlow = (expression, destination, subject) => {
|
|
200
202
|
if (destination === null)
|
|
201
203
|
return;
|
|
@@ -214,7 +216,8 @@ export const noKnownValueWideningRule = defineRule({
|
|
|
214
216
|
const targetFromAnnotation = (annotation) => environment === null ? null : annotationTarget(annotation, environment);
|
|
215
217
|
return {
|
|
216
218
|
Program(node) {
|
|
217
|
-
|
|
219
|
+
hasTypePredicateKeyword = null;
|
|
220
|
+
environment = createTypeEnvironment(node, context.sourceCode.visitorKeys, context.sourceCode.text);
|
|
218
221
|
},
|
|
219
222
|
VariableDeclarator(node) {
|
|
220
223
|
if (node.init === null || node.id.type !== "Identifier")
|
|
@@ -245,6 +248,10 @@ export const noKnownValueWideningRule = defineRule({
|
|
|
245
248
|
CallExpression(node) {
|
|
246
249
|
if (environment === null)
|
|
247
250
|
return;
|
|
251
|
+
// A report needs a local type predicate (`x is T` or `asserts x`) in this file.
|
|
252
|
+
hasTypePredicateKeyword ??= TYPE_PREDICATE_KEYWORD.test(context.sourceCode.text);
|
|
253
|
+
if (!hasTypePredicateKeyword)
|
|
254
|
+
return;
|
|
248
255
|
const owner = localFunctionForCall(context.sourceCode, node.callee);
|
|
249
256
|
if (owner === null)
|
|
250
257
|
return;
|
|
@@ -29,8 +29,6 @@ function isTestFrameworkObject(sourceCode, expression) {
|
|
|
29
29
|
function moduleMockCall(sourceCode, callee) {
|
|
30
30
|
if (!("property" in callee) || !("object" in callee) || !("computed" in callee))
|
|
31
31
|
return false;
|
|
32
|
-
if (!isTestFrameworkObject(sourceCode, callee.object))
|
|
33
|
-
return false;
|
|
34
32
|
const property = callee.property;
|
|
35
33
|
const method = callee.computed
|
|
36
34
|
? property.type === "Literal" &&
|
|
@@ -42,7 +40,10 @@ function moduleMockCall(sourceCode, callee) {
|
|
|
42
40
|
: property.type === "Identifier"
|
|
43
41
|
? property.name
|
|
44
42
|
: null;
|
|
45
|
-
|
|
43
|
+
// Compare the cheap method name before resolving the object through scope analysis.
|
|
44
|
+
return (method !== null &&
|
|
45
|
+
moduleMockMethods.has(method) &&
|
|
46
|
+
isTestFrameworkObject(sourceCode, callee.object));
|
|
46
47
|
}
|
|
47
48
|
/** Ban test framework module mocking in favor of real dependency seams. */
|
|
48
49
|
export const noModuleMockingRule = defineRule({
|
|
@@ -39,7 +39,7 @@ export const noObjectParametersRule = defineRule({
|
|
|
39
39
|
};
|
|
40
40
|
return {
|
|
41
41
|
Program(node) {
|
|
42
|
-
environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys);
|
|
42
|
+
environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys, context.sourceCode.text);
|
|
43
43
|
},
|
|
44
44
|
ArrowFunctionExpression: checkParameters,
|
|
45
45
|
FunctionDeclaration: checkParameters,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
2
|
import { arrayMethodTarget, isKnownArrayExpression, resolveArrayBinding, unwrapArrayExpression, } from "../shared/array-method.js";
|
|
3
|
+
const ARRAY_COPY_METHODS = ["concat", "slice", "toSpliced", "toSorted", "toReversed", "with"];
|
|
3
4
|
function enclosingReducer(node) {
|
|
4
5
|
let parent = node.parent;
|
|
5
6
|
while (parent !== null) {
|
|
@@ -68,6 +69,10 @@ export const noReduceAccumulatorCopyRule = defineRule({
|
|
|
68
69
|
const method = arrayMethodTarget(node.callee);
|
|
69
70
|
if (method === null)
|
|
70
71
|
return;
|
|
72
|
+
// Only these methods can copy an accumulator; skip the parent walk for every other call.
|
|
73
|
+
if (method.name !== "assign" && method.name !== "from" &&
|
|
74
|
+
!ARRAY_COPY_METHODS.includes(method.name))
|
|
75
|
+
return;
|
|
71
76
|
const reducer = enclosingReducer(node);
|
|
72
77
|
if (reducer === null)
|
|
73
78
|
return;
|
|
@@ -85,7 +90,7 @@ export const noReduceAccumulatorCopyRule = defineRule({
|
|
|
85
90
|
const source = node.arguments[0];
|
|
86
91
|
copiesAccumulator = source !== undefined && isAccumulator(source);
|
|
87
92
|
}
|
|
88
|
-
else if (
|
|
93
|
+
else if (ARRAY_COPY_METHODS.includes(method.name)) {
|
|
89
94
|
const initialValue = reducer.initialValue;
|
|
90
95
|
const arrayAccumulator = initialValue !== undefined &&
|
|
91
96
|
isKnownArrayExpression(context.sourceCode, initialValue);
|
|
@@ -13,6 +13,8 @@ export const noReflectApplyRule = defineRule({
|
|
|
13
13
|
},
|
|
14
14
|
createOnce(context) {
|
|
15
15
|
return {
|
|
16
|
+
// Every report calls through the `Reflect` identifier.
|
|
17
|
+
before: () => context.sourceCode.text.includes("Reflect"),
|
|
16
18
|
CallExpression(node) {
|
|
17
19
|
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression")
|
|
18
20
|
return;
|
|
@@ -13,6 +13,8 @@ export const noReflectGetRule = defineRule({
|
|
|
13
13
|
},
|
|
14
14
|
createOnce(context) {
|
|
15
15
|
return {
|
|
16
|
+
// Every report calls through the `Reflect` identifier.
|
|
17
|
+
before: () => context.sourceCode.text.includes("Reflect"),
|
|
16
18
|
CallExpression(node) {
|
|
17
19
|
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression")
|
|
18
20
|
return;
|
|
@@ -47,6 +47,8 @@ export const noRuntimeTypeofRule = defineRule({
|
|
|
47
47
|
},
|
|
48
48
|
createOnce(context) {
|
|
49
49
|
return {
|
|
50
|
+
// Every report is a `typeof` operator, so a file without the keyword cannot report.
|
|
51
|
+
before: () => context.sourceCode.text.includes("typeof"),
|
|
50
52
|
UnaryExpression(node) {
|
|
51
53
|
const option = context.options?.[0];
|
|
52
54
|
const allowInTypeGuards = typeof option === "object" &&
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
2
|
const FORBIDDEN_SYMBOL_NAME = "shape";
|
|
3
|
+
const FORBIDDEN_SYMBOL_TEXT = /shape/i;
|
|
3
4
|
function containsForbiddenSymbolName(name) {
|
|
4
5
|
return name.toLowerCase().includes(FORBIDDEN_SYMBOL_NAME);
|
|
5
6
|
}
|
|
@@ -32,6 +33,8 @@ export const noForbiddenTermInSymbolNamesRule = defineRule({
|
|
|
32
33
|
});
|
|
33
34
|
};
|
|
34
35
|
return {
|
|
36
|
+
// Every symbol name is source text, so a file without the term cannot report.
|
|
37
|
+
before: () => FORBIDDEN_SYMBOL_TEXT.test(context.sourceCode.text),
|
|
35
38
|
Identifier: reportForbiddenSymbolName,
|
|
36
39
|
PrivateIdentifier: reportForbiddenSymbolName,
|
|
37
40
|
JSXIdentifier: reportForbiddenSymbolName,
|
|
@@ -41,7 +41,7 @@ export const noUnknownReturnsRule = defineRule({
|
|
|
41
41
|
};
|
|
42
42
|
return {
|
|
43
43
|
Program(node) {
|
|
44
|
-
environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys);
|
|
44
|
+
environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys, context.sourceCode.text);
|
|
45
45
|
},
|
|
46
46
|
ArrowFunctionExpression: checkReturnType,
|
|
47
47
|
FunctionDeclaration: checkReturnType,
|
|
@@ -24,7 +24,7 @@ export const noUnknownTypeAliasesRule = defineRule({
|
|
|
24
24
|
});
|
|
25
25
|
return {
|
|
26
26
|
Program(node) {
|
|
27
|
-
environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys);
|
|
27
|
+
environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys, context.sourceCode.text);
|
|
28
28
|
},
|
|
29
29
|
TSTypeAliasDeclaration(node) {
|
|
30
30
|
if (!resolvesToUnknown(node.typeAnnotation))
|
|
@@ -115,7 +115,7 @@ export const noUnsafeDictionaryTypeRule = defineRule({
|
|
|
115
115
|
};
|
|
116
116
|
return {
|
|
117
117
|
Program(node) {
|
|
118
|
-
environment = createTypeEnvironment(node, context.sourceCode.visitorKeys);
|
|
118
|
+
environment = createTypeEnvironment(node, context.sourceCode.visitorKeys, context.sourceCode.text);
|
|
119
119
|
},
|
|
120
120
|
TSTypeReference: reportIfUnsafe,
|
|
121
121
|
TSTypeLiteral: reportIfUnsafe,
|
|
@@ -238,11 +238,13 @@ export const noWidenThenAssertRule = defineRule({
|
|
|
238
238
|
},
|
|
239
239
|
},
|
|
240
240
|
createOnce(context) {
|
|
241
|
-
let
|
|
241
|
+
let fileScopes = null;
|
|
242
242
|
const checkAssertion = (node) => {
|
|
243
243
|
const expression = assertedExpression(node);
|
|
244
244
|
if (expression.type !== "Identifier")
|
|
245
245
|
return;
|
|
246
|
+
// Scope analysis is costly; read it only once a candidate assertion exists.
|
|
247
|
+
const scopes = (fileScopes ??= context.sourceCode.scopeManager.scopes);
|
|
246
248
|
const variable = resolvedVariableForIdentifier(scopes, expression);
|
|
247
249
|
if (variable === null)
|
|
248
250
|
return;
|
|
@@ -261,7 +263,7 @@ export const noWidenThenAssertRule = defineRule({
|
|
|
261
263
|
};
|
|
262
264
|
return {
|
|
263
265
|
Program() {
|
|
264
|
-
|
|
266
|
+
fileScopes = null;
|
|
265
267
|
},
|
|
266
268
|
TSAsExpression: checkAssertion,
|
|
267
269
|
TSTypeAssertion: checkAssertion,
|
|
@@ -12,7 +12,7 @@ export type TypeEnvironment = {
|
|
|
12
12
|
readonly interfaces: ReadonlyMap<string, readonly ESTree.TSInterfaceDeclaration[]>;
|
|
13
13
|
readonly typeAliases: LexicalTypeAliasEnvironment;
|
|
14
14
|
};
|
|
15
|
-
export declare function createTypeEnvironment(program: ESTree.Program, visitorKeys: Readonly<Record<string, readonly string[]
|
|
15
|
+
export declare function createTypeEnvironment(program: ESTree.Program, visitorKeys: Readonly<Record<string, readonly string[]>>, sourceText: string): TypeEnvironment;
|
|
16
16
|
export declare function classifyUnsafeDictionaryValue(valueType: ESTree.TSType, environment: TypeEnvironment): UnsafeDictionary | null;
|
|
17
17
|
export declare function classifyUnsafeDictionary(type: ESTree.TSType, environment: TypeEnvironment): UnsafeDictionary | null;
|
|
18
18
|
export declare function classifyWideningTarget(type: ESTree.TSType, environment: TypeEnvironment): WideningTarget | null;
|
|
@@ -16,7 +16,7 @@ function declaredStatement(statement) {
|
|
|
16
16
|
? (statement.declaration ?? null)
|
|
17
17
|
: statement;
|
|
18
18
|
}
|
|
19
|
-
export function createTypeEnvironment(program, visitorKeys) {
|
|
19
|
+
export function createTypeEnvironment(program, visitorKeys, sourceText) {
|
|
20
20
|
const interfaces = new Map();
|
|
21
21
|
for (const statement of program.body) {
|
|
22
22
|
const declaration = declaredStatement(statement);
|
|
@@ -28,7 +28,7 @@ export function createTypeEnvironment(program, visitorKeys) {
|
|
|
28
28
|
}
|
|
29
29
|
return {
|
|
30
30
|
interfaces,
|
|
31
|
-
typeAliases: createTypeAliasEnvironment(program, visitorKeys),
|
|
31
|
+
typeAliases: createTypeAliasEnvironment(program, visitorKeys, sourceText),
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
function typeReferenceName(type) {
|
|
@@ -12,8 +12,8 @@ export type TypeAliasEnvironment = {
|
|
|
12
12
|
readonly visitorKeys: VisitorKeys;
|
|
13
13
|
};
|
|
14
14
|
export type ResolvedTypeMatcher = (type: ESTree.TSType, matches: (child: ESTree.TSType) => boolean) => boolean;
|
|
15
|
-
/** Collect every lexical type alias and competing type binding in a program. */
|
|
16
|
-
export declare function createTypeAliasEnvironment(program: ESTree.Program, visitorKeys: VisitorKeys): TypeAliasEnvironment;
|
|
15
|
+
/** Collect every lexical type alias and competing type binding in a program, on first use. */
|
|
16
|
+
export declare function createTypeAliasEnvironment(program: ESTree.Program, visitorKeys: VisitorKeys, sourceText: string): TypeAliasEnvironment;
|
|
17
17
|
/** Resolve the nearest visible alias with this name, respecting lexical shadowing. */
|
|
18
18
|
export declare function visibleTypeAlias(name: string, use: ESTree.Node, environment: TypeAliasEnvironment): ESTree.TSTypeAliasDeclaration | null;
|
|
19
19
|
/** Return whether a local declaration shadows a built-in type at this use. */
|
|
@@ -37,41 +37,84 @@ function declaredTypeBinding(node) {
|
|
|
37
37
|
}
|
|
38
38
|
return null;
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Every node that declares a type binding contains one of these keywords in its own source
|
|
42
|
+
* range. Import specifiers are the exception; they are reached through their ImportDeclaration.
|
|
43
|
+
*/
|
|
44
|
+
const BINDING_KEYWORD = /\b(?:class|enum|import|interface|type)\b/g;
|
|
45
|
+
function bindingKeywordOffsets(sourceText) {
|
|
46
|
+
const offsets = [];
|
|
47
|
+
for (const match of sourceText.matchAll(BINDING_KEYWORD))
|
|
48
|
+
offsets.push(match.index);
|
|
49
|
+
return offsets;
|
|
50
|
+
}
|
|
51
|
+
/** Return whether a sorted offset list has an entry in `[start, end)`. */
|
|
52
|
+
function hasOffsetInRange(offsets, start, end) {
|
|
53
|
+
let low = 0;
|
|
54
|
+
let high = offsets.length;
|
|
55
|
+
while (low < high) {
|
|
56
|
+
const middle = (low + high) >>> 1;
|
|
57
|
+
if ((offsets[middle] ?? 0) < start)
|
|
58
|
+
low = middle + 1;
|
|
59
|
+
else
|
|
60
|
+
high = middle;
|
|
61
|
+
}
|
|
62
|
+
return low < offsets.length && (offsets[low] ?? end) < end;
|
|
63
|
+
}
|
|
64
|
+
function collectTypeBindings(node, visitorKeys, offsets, visitAll, collection) {
|
|
41
65
|
const declared = declaredTypeBinding(node);
|
|
42
66
|
if (declared !== null) {
|
|
43
|
-
const bindings = bindingsByName.get(declared.name) ?? [];
|
|
67
|
+
const bindings = collection.bindingsByName.get(declared.name) ?? [];
|
|
44
68
|
bindings.push({ ...declared, scope: enclosingTypeScope(node) });
|
|
45
|
-
bindingsByName.set(declared.name, bindings);
|
|
69
|
+
collection.bindingsByName.set(declared.name, bindings);
|
|
46
70
|
if (declared.alias !== null)
|
|
47
|
-
aliases.push(declared.alias);
|
|
71
|
+
collection.aliases.push(declared.alias);
|
|
48
72
|
}
|
|
73
|
+
const visitChildren = visitAll || node.type === "ImportDeclaration";
|
|
49
74
|
// SAFETY: Oxlint's visitor keys identify only ESTree child-node properties.
|
|
50
75
|
const fields = node;
|
|
51
76
|
for (const key of visitorKeys[node.type] ?? []) {
|
|
52
77
|
const value = fields[key];
|
|
53
78
|
if (isNode(value)) {
|
|
54
|
-
|
|
79
|
+
if (visitChildren || hasOffsetInRange(offsets, value.start, value.end)) {
|
|
80
|
+
collectTypeBindings(value, visitorKeys, offsets, visitChildren, collection);
|
|
81
|
+
}
|
|
55
82
|
continue;
|
|
56
83
|
}
|
|
57
84
|
if (!Array.isArray(value))
|
|
58
85
|
continue;
|
|
59
86
|
for (const child of value) {
|
|
60
|
-
if (isNode(child)
|
|
61
|
-
|
|
87
|
+
if (isNode(child) &&
|
|
88
|
+
(visitChildren || hasOffsetInRange(offsets, child.start, child.end))) {
|
|
89
|
+
collectTypeBindings(child, visitorKeys, offsets, visitChildren, collection);
|
|
62
90
|
}
|
|
63
91
|
}
|
|
64
92
|
}
|
|
65
93
|
}
|
|
66
|
-
|
|
67
|
-
|
|
94
|
+
function collectProgramTypeBindings(program, visitorKeys, sourceText) {
|
|
95
|
+
const collection = { aliases: [], bindingsByName: new Map() };
|
|
96
|
+
const offsets = bindingKeywordOffsets(sourceText);
|
|
97
|
+
if (offsets.length > 0) {
|
|
98
|
+
collectTypeBindings(program, visitorKeys, offsets, false, collection);
|
|
99
|
+
}
|
|
100
|
+
return collection;
|
|
101
|
+
}
|
|
102
|
+
/** Collect every lexical type alias and competing type binding in a program, on first use. */
|
|
103
|
+
export function createTypeAliasEnvironment(program, visitorKeys, sourceText) {
|
|
68
104
|
const cached = environmentsByProgram.get(program);
|
|
69
105
|
if (cached !== undefined)
|
|
70
106
|
return cached;
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
107
|
+
let collection = null;
|
|
108
|
+
const collected = () => (collection ??= collectProgramTypeBindings(program, visitorKeys, sourceText));
|
|
109
|
+
const environment = {
|
|
110
|
+
get aliases() {
|
|
111
|
+
return collected().aliases;
|
|
112
|
+
},
|
|
113
|
+
get bindingsByName() {
|
|
114
|
+
return collected().bindingsByName;
|
|
115
|
+
},
|
|
116
|
+
visitorKeys,
|
|
117
|
+
};
|
|
75
118
|
environmentsByProgram.set(program, environment);
|
|
76
119
|
return environment;
|
|
77
120
|
}
|