eslint-plugin-no-mistakes 0.99.0 → 0.100.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/README.md +12 -5
- package/package.json +9 -8
- package/src/adjacent-annotation.js +72 -0
- package/src/index.js +1 -0
- package/src/rules/async-call-disposition.js +15 -3
- package/src/rules/async-consumer-cache-proof.js +5 -5
- package/src/rules/async-observer-mutations.js +4 -1
- package/src/rules/async-source-candidates.js +35 -0
- package/src/rules/async-source-declarations.js +35 -18
- package/src/rules/async-source-files.js +42 -0
- package/src/rules/async-source-owner.js +75 -0
- package/src/rules/async-source-session.js +91 -0
- package/src/rules/async-source-store.js +30 -81
- package/src/rules/async-source-ts-declarations.js +184 -0
- package/src/rules/async-source-ts-expressions.js +201 -0
- package/src/rules/async-source-ts-parser.js +94 -0
- package/src/rules/async-source-ts-references.js +82 -0
- package/src/rules/async-source-ts-scopes.js +153 -0
- package/src/rules/async-source-ts-statements.js +102 -0
- package/src/rules/async-source-ts-templates.js +20 -0
- package/src/rules/async-source-ts-types.js +49 -0
- package/src/rules/async-source-types.js +19 -1
- package/src/rules/async-target-bindings.js +22 -0
- package/src/rules/playwright-no-swallowed-visibility.js +166 -0
- package/src/rules/playwright-test-timeout-bindings.js +2 -0
- package/src/rules/test-no-skips-bindings.js +28 -5
- package/src/rules/vitest-timeout-cap.js +15 -0
- package/src/rules/vitest-timeout-reasons.js +19 -0
- package/src/rules/vitest-timeout-values.js +5 -1
package/README.md
CHANGED
|
@@ -11,14 +11,16 @@ npm install --save-dev eslint-plugin-no-mistakes
|
|
|
11
11
|
|
|
12
12
|
For `async-call-disposition` to inspect imported callback implementations when
|
|
13
13
|
the linter does not provide a TypeScript program (including Oxlint's
|
|
14
|
-
`--type-aware` mode), also install `typescript`
|
|
15
|
-
`@typescript-eslint/parser` in your project:
|
|
14
|
+
`--type-aware` mode), also install `typescript` in your project:
|
|
16
15
|
|
|
17
16
|
```sh
|
|
18
|
-
npm install --save-dev typescript
|
|
17
|
+
npm install --save-dev typescript
|
|
19
18
|
```
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
`typescript` and `@typescript-eslint/parser` are optional peers, but only
|
|
21
|
+
`typescript` is required for the cross-file proof. When the ESLint parser is
|
|
22
|
+
absent, the proof parses supported syntax with `ts.createSourceFile`; unsupported
|
|
23
|
+
syntax stays unproven. This fallback requires TypeScript 5 or
|
|
22
24
|
later for Bundler module resolution. With older TypeScript or missing or
|
|
23
25
|
incompatible tooling, calls that rely on this cross-file proof are reported as
|
|
24
26
|
unproven. Same-file proofs and explicitly configured awaiting-consumer
|
|
@@ -48,7 +50,9 @@ for the complete list.
|
|
|
48
50
|
To cap long Vitest timeouts, enable `no-mistakes/vitest-timeout-cap` in the
|
|
49
51
|
test and Vitest config files selected by your flat-config globs. It defaults to
|
|
50
52
|
5,000 ms for configured test and hook timeouts and 30,000 ms for per-test and
|
|
51
|
-
hook overrides.
|
|
53
|
+
hook overrides. Overrides above the default cap require an adjacent
|
|
54
|
+
`// no-mistakes-timeout-reason: <specific reason>` comment.
|
|
55
|
+
See the [rule documentation](../../docs/eslint-rules/vitest-timeout-cap.md)
|
|
52
56
|
for options and examples.
|
|
53
57
|
|
|
54
58
|
`gh-api-argument-safety` is an opt-in rule for structured endpoint fields and explicit API methods.
|
|
@@ -63,6 +67,9 @@ See [options and examples](../../docs/eslint-rules/no-comment-phrases.md).
|
|
|
63
67
|
`forbidden-member-chains` is an opt-in rule for configured static member-access chains.
|
|
64
68
|
See [options and examples](../../docs/eslint-rules/forbidden-member-chains.md).
|
|
65
69
|
|
|
70
|
+
`playwright-no-swallowed-visibility` is an opt-in rule for Playwright state checks whose rejection becomes a boolean literal.
|
|
71
|
+
See [options and examples](../../docs/eslint-rules/playwright-no-swallowed-visibility.md).
|
|
72
|
+
|
|
66
73
|
`no-object-mutation` is an opt-in rule for shared object writes and mutating calls.
|
|
67
74
|
See [options and examples](../../docs/eslint-rules/no-object-mutation.md).
|
|
68
75
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-no-mistakes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.100.0",
|
|
4
4
|
"description": "ESLint and Oxlint rules that keep code static enough for no-mistakes analyzers and coding agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -21,17 +21,17 @@
|
|
|
21
21
|
"test": "vitest run --coverage"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@typescript-eslint/parser": "^8.
|
|
25
|
-
"@vitest/coverage-v8": "^5.0.
|
|
26
|
-
"eslint": "^10.
|
|
27
|
-
"oxlint": "^1.
|
|
24
|
+
"@typescript-eslint/parser": "^8.72.0",
|
|
25
|
+
"@vitest/coverage-v8": "^5.0.5",
|
|
26
|
+
"eslint": "^10.13.0",
|
|
27
|
+
"oxlint": "^1.89.0",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
29
|
-
"vitest": "^5.0.
|
|
29
|
+
"vitest": "^5.0.5"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"@typescript-eslint/parser": ">=8",
|
|
33
33
|
"eslint": ">=9",
|
|
34
|
-
"typescript": ">=
|
|
34
|
+
"typescript": ">=5"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
37
37
|
"@typescript-eslint/parser": {
|
|
@@ -43,5 +43,6 @@
|
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": "^20.19.0 || ^22.13.0 || >=24"
|
|
46
|
-
}
|
|
46
|
+
},
|
|
47
|
+
"//": "typescript stays on 6.0.3. TypeScript 7 makes the typescript-eslint parser throw on load. https://github.com/typescript-eslint/typescript-eslint/issues/12518 Related: https://github.com/typescript-eslint/typescript-eslint/issues/10940"
|
|
47
48
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Parentheses required by a parent construct are not grouping around the target.
|
|
4
|
+
function syntaxParen(sourceCode, node) {
|
|
5
|
+
// ESTree wraps an optional registration without introducing any syntax.
|
|
6
|
+
while (node.parent?.type === "ChainExpression") node = node.parent;
|
|
7
|
+
const parent = node.parent;
|
|
8
|
+
if (!parent) return null;
|
|
9
|
+
if (["CallExpression", "NewExpression"].includes(parent.type)) {
|
|
10
|
+
return sourceCode.getTokenAfter(
|
|
11
|
+
parent.typeArguments || parent.typeParameters || parent.callee,
|
|
12
|
+
{
|
|
13
|
+
filter: (token) => token.value === "(",
|
|
14
|
+
},
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
if (
|
|
18
|
+
[
|
|
19
|
+
"IfStatement",
|
|
20
|
+
"WhileStatement",
|
|
21
|
+
"SwitchStatement",
|
|
22
|
+
"WithStatement",
|
|
23
|
+
"ImportExpression",
|
|
24
|
+
].includes(parent.type)
|
|
25
|
+
)
|
|
26
|
+
return sourceCode.getTokenAfter(sourceCode.getFirstToken(parent));
|
|
27
|
+
if (parent.type === "DoWhileStatement")
|
|
28
|
+
return sourceCode.getTokenAfter(sourceCode.getTokenAfter(parent.body));
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Only comments, whitespace and grouping may separate an annotation and its target.
|
|
33
|
+
// A comment trailing another statement belongs to that statement, never the target.
|
|
34
|
+
function adjacentAnnotations(sourceCode, node, marker) {
|
|
35
|
+
const comments = [];
|
|
36
|
+
let left = node;
|
|
37
|
+
let right = node;
|
|
38
|
+
const boundary = syntaxParen(sourceCode, node);
|
|
39
|
+
for (;;) {
|
|
40
|
+
const previous = sourceCode.getTokenBefore(left, { includeComments: true });
|
|
41
|
+
if (!previous || left.loc.start.line - previous.loc.end.line > 1) break;
|
|
42
|
+
if (["Line", "Block"].includes(previous.type)) {
|
|
43
|
+
comments.push(previous);
|
|
44
|
+
left = previous;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const closing = sourceCode.getTokenAfter(right);
|
|
48
|
+
if (
|
|
49
|
+
previous.value !== "(" ||
|
|
50
|
+
closing?.value !== ")" ||
|
|
51
|
+
previous.range[0] === boundary?.range[0]
|
|
52
|
+
)
|
|
53
|
+
break;
|
|
54
|
+
left = previous;
|
|
55
|
+
right = closing;
|
|
56
|
+
}
|
|
57
|
+
const result = [];
|
|
58
|
+
for (const comment of comments) {
|
|
59
|
+
const previous = sourceCode.getTokenBefore(comment);
|
|
60
|
+
if (
|
|
61
|
+
previous?.loc.end.line === comment.loc.start.line &&
|
|
62
|
+
!["(", "[", "{", ",", ":"].includes(previous.value)
|
|
63
|
+
)
|
|
64
|
+
break;
|
|
65
|
+
for (const text of comment.value.split(/\r?\n/)) {
|
|
66
|
+
const normalized = text.replace(/^\s*\*?\s?/, "");
|
|
67
|
+
if (normalized.startsWith(marker)) result.push(normalized.slice(marker.length).trim());
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
module.exports = { adjacentAnnotations };
|
package/src/index.js
CHANGED
|
@@ -34,6 +34,7 @@ const rules = {
|
|
|
34
34
|
"playwright-no-empty": require("./rules/playwright-no-empty"),
|
|
35
35
|
"playwright-no-hoisted-unique-token": require("./rules/playwright-no-hoisted-unique-token"),
|
|
36
36
|
"playwright-no-raw-scroll-pagination": require("./rules/playwright-no-raw-scroll-pagination"),
|
|
37
|
+
"playwright-no-swallowed-visibility": require("./rules/playwright-no-swallowed-visibility"),
|
|
37
38
|
"playwright-prefer-get-by-test-id": require("./rules/playwright-prefer-get-by-test-id"),
|
|
38
39
|
"playwright-require-exported-component-attribute": require("./rules/playwright-require-exported-component-attribute"),
|
|
39
40
|
"playwright-require-interactive-test-id": require("./rules/playwright-require-interactive-test-id"),
|
|
@@ -19,6 +19,8 @@ const { createTargetMatcher } = require("./async-targets");
|
|
|
19
19
|
const { promiseChainFrom } = require("./async-disposition-observers");
|
|
20
20
|
const { createAwaitingConsumers } = require("./async-disposition-consumers");
|
|
21
21
|
const { createPromiseHandoff } = require("./async-promise-handoff");
|
|
22
|
+
const { possibleValueType } = require("./async-source-candidates");
|
|
23
|
+
const { sourcesUnavailable, withSourceOwner } = require("./async-source-owner");
|
|
22
24
|
|
|
23
25
|
function isDisposed(node, context, consumers, seen = new Set(), catchProven = true) {
|
|
24
26
|
if (seen.has(node)) return false;
|
|
@@ -43,7 +45,7 @@ function isDisposed(node, context, consumers, seen = new Set(), catchProven = tr
|
|
|
43
45
|
);
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
const dispositionRule = rule(
|
|
47
49
|
{
|
|
48
50
|
type: "problem",
|
|
49
51
|
docs: {
|
|
@@ -60,7 +62,11 @@ module.exports = rule(
|
|
|
60
62
|
},
|
|
61
63
|
(context) => {
|
|
62
64
|
const matcher = createTargetMatcher(context);
|
|
63
|
-
const sourceFacts = {
|
|
65
|
+
const sourceFacts = {
|
|
66
|
+
typed: new WeakMap(),
|
|
67
|
+
sources: sourcesUnavailable(context) ? false : null,
|
|
68
|
+
targets: matcher.targets,
|
|
69
|
+
};
|
|
64
70
|
const { promiseSlotHandoff, promiseTypedTargetCall } = createPromiseHandoff(
|
|
65
71
|
context,
|
|
66
72
|
sourceFacts,
|
|
@@ -89,9 +95,13 @@ module.exports = rule(
|
|
|
89
95
|
),
|
|
90
96
|
CallExpression(node) {
|
|
91
97
|
if (suppressed(node)) return;
|
|
98
|
+
const target = matcher.isTargetCall(node);
|
|
99
|
+
// Unrelated calls cannot need a callback-body or imported type proof.
|
|
100
|
+
if (!target && (sourceFacts.sources === false || !possibleValueType(node.callee, context)))
|
|
101
|
+
return;
|
|
92
102
|
// Disposed calls need no cross-file type lookup, including local/configured JS consumers.
|
|
93
103
|
if (isDisposed(node, context, consumers)) return;
|
|
94
|
-
if (!
|
|
104
|
+
if (!target && !promiseTypedTargetCall(node)) return;
|
|
95
105
|
context.report({
|
|
96
106
|
node,
|
|
97
107
|
messageId: "disposition",
|
|
@@ -111,3 +121,5 @@ module.exports = rule(
|
|
|
111
121
|
};
|
|
112
122
|
},
|
|
113
123
|
);
|
|
124
|
+
|
|
125
|
+
module.exports = withSourceOwner(dispositionRule);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const { resolveVariable } = require("./async-target-bindings");
|
|
3
|
+
const { resolveVariable, resolveRuntimeVariable } = require("./async-target-bindings");
|
|
4
4
|
const { propertiesWritten } = require("./async-disposition-mocks");
|
|
5
5
|
|
|
6
6
|
function sameKey(first, key, context) {
|
|
@@ -34,18 +34,18 @@ function builtinMap(node, context) {
|
|
|
34
34
|
references.some(
|
|
35
35
|
(ref) =>
|
|
36
36
|
["eval", "Function"].includes(ref.identifier.name) &&
|
|
37
|
-
!
|
|
37
|
+
!resolveRuntimeVariable(ref.identifier, context)?.defs.length,
|
|
38
38
|
)
|
|
39
39
|
)
|
|
40
40
|
return false;
|
|
41
|
-
const binding =
|
|
41
|
+
const binding = resolveRuntimeVariable(node, context);
|
|
42
42
|
// Do not infer built-in semantics after the constructor/prototype escapes to
|
|
43
43
|
// reflective APIs or unknown consumers, whose effects are not modeled here.
|
|
44
44
|
if (
|
|
45
45
|
references.some(
|
|
46
46
|
(ref) =>
|
|
47
47
|
ref.identifier.name === "Map" &&
|
|
48
|
-
|
|
48
|
+
resolveRuntimeVariable(ref.identifier, context) === binding &&
|
|
49
49
|
!(
|
|
50
50
|
ref.identifier.parent.type === "NewExpression" &&
|
|
51
51
|
ref.identifier.parent.callee === ref.identifier
|
|
@@ -56,7 +56,7 @@ function builtinMap(node, context) {
|
|
|
56
56
|
const holders = ["globalThis", "global", "window", "self"];
|
|
57
57
|
return !references.some((ref) => {
|
|
58
58
|
if (!holders.includes(ref.identifier.name)) return false;
|
|
59
|
-
if (
|
|
59
|
+
if (resolveRuntimeVariable(ref.identifier, context)?.defs.length) return false;
|
|
60
60
|
if (propertiesWritten(ref.identifier, context)) return true;
|
|
61
61
|
const member = ref.identifier.parent;
|
|
62
62
|
if (member.type !== "MemberExpression" || member.object !== ref.identifier) return true;
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const { isPromiseMethodCall } = require("./async-disposition-bindings");
|
|
4
4
|
const { unwrapExpression } = require("./async-ast");
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
resolveRuntimeVariable: resolveVariable,
|
|
7
|
+
memberPropertyName,
|
|
8
|
+
} = require("./async-target-bindings");
|
|
6
9
|
|
|
7
10
|
function observerMutated(node, context, suffix = []) {
|
|
8
11
|
function paths(value, seen = new Set()) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { resolveVariable, stableConstInitializer } = require("./async-target-bindings");
|
|
4
|
+
|
|
5
|
+
const sourceTypeShapes = new Set([
|
|
6
|
+
"TSTypeReference",
|
|
7
|
+
"TSTypeQuery",
|
|
8
|
+
"TSTypeLiteral",
|
|
9
|
+
"TSIntersectionType",
|
|
10
|
+
"TSParenthesizedType",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
// Mirror the shapes valueType can follow without resolving or parsing imports.
|
|
14
|
+
// In particular, an import or function declaration alone has no receiving type.
|
|
15
|
+
function possibleValueType(value, context, seen = new Set()) {
|
|
16
|
+
if (!value || seen.size >= 32 || seen.has(value)) return false;
|
|
17
|
+
const next = new Set(seen).add(value);
|
|
18
|
+
if (value.type === "Identifier") {
|
|
19
|
+
const variable = resolveVariable(value, context);
|
|
20
|
+
if (variable?.defs.length !== 1) return false;
|
|
21
|
+
const type = variable.defs[0].name?.typeAnnotation?.typeAnnotation;
|
|
22
|
+
// Arrays, function signatures, and primitive annotations cannot establish
|
|
23
|
+
// targetType or a property contract in the bounded source type model.
|
|
24
|
+
if (type) return sourceTypeShapes.has(type.type);
|
|
25
|
+
if (variable.references.some((ref) => ref.isWrite() && !ref.init)) return false;
|
|
26
|
+
const init = stableConstInitializer(value, context, new Set());
|
|
27
|
+
return possibleValueType(init, context, next);
|
|
28
|
+
}
|
|
29
|
+
// declarations.callable only resolves identifier callees, including imported
|
|
30
|
+
// factories with a return-type alias. Member calls and IIFEs cannot contribute.
|
|
31
|
+
if (value.type === "CallExpression") return !value.optional && value.callee.type === "Identifier";
|
|
32
|
+
return value.type === "MemberExpression" && possibleValueType(value.object, context, next);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { possibleValueType };
|
|
@@ -6,14 +6,14 @@ const { unwrapExpression } = require("./async-ast");
|
|
|
6
6
|
// Follow bounded static declarations. Missing source, cycles, and changing bindings
|
|
7
7
|
// cannot establish the identity of a configured target or dependency type.
|
|
8
8
|
function createDeclarations(sources) {
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
seen = new Set(seen).add(key);
|
|
13
|
-
const source = sources.load(filename);
|
|
14
|
-
if (!source) return false;
|
|
15
|
-
const matches = [];
|
|
9
|
+
function exportIndex(source) {
|
|
10
|
+
if (source.exportIndex) return source.exportIndex;
|
|
11
|
+
const names = new Map();
|
|
16
12
|
const stars = [];
|
|
13
|
+
function add(name, match) {
|
|
14
|
+
if (!names.has(name)) names.set(name, []);
|
|
15
|
+
names.get(name).push(match);
|
|
16
|
+
}
|
|
17
17
|
for (const statement of source.ast.body) {
|
|
18
18
|
if (statement.type === "ExportAllDeclaration") {
|
|
19
19
|
if (!statement.exported) stars.push(statement.source.value);
|
|
@@ -21,24 +21,41 @@ function createDeclarations(sources) {
|
|
|
21
21
|
}
|
|
22
22
|
if (statement.type !== "ExportNamedDeclaration") continue;
|
|
23
23
|
const declaration = statement.declaration;
|
|
24
|
-
if (declaration?.id?.name
|
|
24
|
+
if (declaration?.id?.name) add(declaration.id.name, { local: declaration.id });
|
|
25
25
|
if (declaration?.type === "VariableDeclaration")
|
|
26
26
|
for (const item of declaration.declarations)
|
|
27
|
-
if (item.id.type === "Identifier"
|
|
28
|
-
matches.push({ local: item.id });
|
|
27
|
+
if (item.id.type === "Identifier") add(item.id.name, { local: item.id });
|
|
29
28
|
for (const item of statement.specifiers)
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
add(item.exported.name ?? item.exported.value, {
|
|
30
|
+
local: item.local,
|
|
31
|
+
source: statement.source,
|
|
32
|
+
});
|
|
32
33
|
}
|
|
34
|
+
const index = { names, stars };
|
|
35
|
+
source.exportIndex = index;
|
|
36
|
+
return index;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function exported(filename, name, seen = new Set(), budget = { remaining: 128 }) {
|
|
40
|
+
const key = `${filename}:${name}`;
|
|
41
|
+
if (budget.remaining-- <= 0 || seen.size >= 32 || seen.has(key)) return false;
|
|
42
|
+
seen = new Set(seen).add(key);
|
|
43
|
+
const source = sources.load(filename);
|
|
44
|
+
if (!source) return false;
|
|
45
|
+
const { names, stars } = exportIndex(source);
|
|
46
|
+
const matches = names.get(name) || [];
|
|
33
47
|
if (matches.length > 1) return false;
|
|
34
48
|
if (!matches.length) {
|
|
35
|
-
const
|
|
49
|
+
const found = [];
|
|
50
|
+
for (const specifier of stars) {
|
|
51
|
+
// Bound resolution work too, including cached or unresolved branches.
|
|
52
|
+
if (budget.remaining <= 0) return false;
|
|
36
53
|
const target = sources.resolve(specifier, filename);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
const candidate = target ? exported(target, name, seen, budget) : false;
|
|
55
|
+
// An unreadable branch may also export this name; absence is not proof.
|
|
56
|
+
if (candidate === false) return false;
|
|
57
|
+
if (candidate) found.push(candidate);
|
|
58
|
+
}
|
|
42
59
|
return found.length > 1 ? false : found[0] || null;
|
|
43
60
|
}
|
|
44
61
|
const match = matches[0];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function parsedSource(filename, text, parser) {
|
|
4
|
+
const parsed = parser.parseForESLint(text, { filePath: filename, range: true, loc: true });
|
|
5
|
+
function parents(node, parent) {
|
|
6
|
+
node.parent = parent;
|
|
7
|
+
for (const key of parsed.visitorKeys[node.type] || []) {
|
|
8
|
+
const child = node[key];
|
|
9
|
+
if (Array.isArray(child)) child.forEach((item) => item && parents(item, node));
|
|
10
|
+
else if (child) parents(child, node);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
parents(parsed.ast, null);
|
|
14
|
+
const sourceCode = {
|
|
15
|
+
...parsed,
|
|
16
|
+
getScope(node) {
|
|
17
|
+
// Every parsed node has a Program ancestor with a global scope.
|
|
18
|
+
for (let owner = node; ; owner = owner.parent) {
|
|
19
|
+
const scope = parsed.scopeManager.acquire(owner, true);
|
|
20
|
+
if (scope) return scope;
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
return { ast: parsed.ast, context: { sourceCode, filename } };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function currentSources(context, store) {
|
|
28
|
+
const filename = context.filename;
|
|
29
|
+
let main;
|
|
30
|
+
return {
|
|
31
|
+
resolve: store.resolve,
|
|
32
|
+
load(path) {
|
|
33
|
+
// Oxlint reuses its Context/SourceCode/AST. Keep this overlay per file;
|
|
34
|
+
// the shared store contains only independently parsed imported sources.
|
|
35
|
+
return path === filename
|
|
36
|
+
? (main ||= { ast: context.sourceCode.ast, context })
|
|
37
|
+
: store.load(path);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { parsedSource, currentSources };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { readFileSync, realpathSync } = require("node:fs");
|
|
4
|
+
const { dirname, resolve } = require("node:path");
|
|
5
|
+
|
|
6
|
+
// The public npm CLI calls native lint once. Editors, fix passes, and unknown
|
|
7
|
+
// hosts have no invocation boundary available to a JS plugin, so stay per-file.
|
|
8
|
+
function oneShotCli(entry = process.argv[1], args = process.argv.slice(2)) {
|
|
9
|
+
if (!entry || args.some((arg) => /^--(?:lsp|watch)(?:=|$)/.test(arg) || arg.startsWith("--fix")))
|
|
10
|
+
return false;
|
|
11
|
+
try {
|
|
12
|
+
const filename = realpathSync(entry);
|
|
13
|
+
const root = resolve(dirname(filename), "..");
|
|
14
|
+
const manifest = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
|
|
15
|
+
return (
|
|
16
|
+
manifest.name === "oxlint" &&
|
|
17
|
+
typeof manifest.bin?.oxlint === "string" &&
|
|
18
|
+
realpathSync(resolve(root, manifest.bin.oxlint)) === filename
|
|
19
|
+
);
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const activeOwners = new WeakMap();
|
|
26
|
+
|
|
27
|
+
function sourceOwner(context) {
|
|
28
|
+
const owner = activeOwners.get(context);
|
|
29
|
+
if (!owner) return null;
|
|
30
|
+
owner.shared ??= oneShotCli();
|
|
31
|
+
return owner.shared ? owner : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sourcesUnavailable(context) {
|
|
35
|
+
return activeOwners.get(context)?.sources === false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function withSourceOwner(rule) {
|
|
39
|
+
rule.createOnce = (context) => {
|
|
40
|
+
const owner = { sources: null };
|
|
41
|
+
let visitor;
|
|
42
|
+
return {
|
|
43
|
+
...Object.fromEntries(
|
|
44
|
+
[
|
|
45
|
+
"Program",
|
|
46
|
+
"ImportDeclaration",
|
|
47
|
+
"TSImportEqualsDeclaration",
|
|
48
|
+
"VariableDeclarator",
|
|
49
|
+
"CallExpression",
|
|
50
|
+
].map((key) => [key, (node) => visitor[key]?.(node)]),
|
|
51
|
+
),
|
|
52
|
+
before() {
|
|
53
|
+
activeOwners.set(context, owner);
|
|
54
|
+
try {
|
|
55
|
+
visitor = rule.create(context);
|
|
56
|
+
if (Object.keys(visitor).length === 0) {
|
|
57
|
+
visitor = null;
|
|
58
|
+
activeOwners.delete(context);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
activeOwners.delete(context);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
after() {
|
|
67
|
+
visitor = null;
|
|
68
|
+
activeOwners.delete(context);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
return rule;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { oneShotCli, sourceOwner, sourcesUnavailable, withSourceOwner };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { dirname } = require("node:path");
|
|
4
|
+
const { parsedSource } = require("./async-source-files");
|
|
5
|
+
|
|
6
|
+
function createSourceSession(ts, parser) {
|
|
7
|
+
// Legacy resolution may inspect `main` while runtime `exports` selects another body.
|
|
8
|
+
if (
|
|
9
|
+
ts.ModuleResolutionKind.Bundler === undefined ||
|
|
10
|
+
typeof ts.createModuleResolutionCache !== "function"
|
|
11
|
+
)
|
|
12
|
+
return null;
|
|
13
|
+
const files = new Map();
|
|
14
|
+
const configs = new Map();
|
|
15
|
+
const configFiles = new Map();
|
|
16
|
+
const resolved = new Map();
|
|
17
|
+
const resolutionCaches = new Map();
|
|
18
|
+
const host = { ...ts.sys };
|
|
19
|
+
for (const method of ["readFile", "fileExists", "directoryExists", "realpath"]) {
|
|
20
|
+
if (!ts.sys[method]) continue;
|
|
21
|
+
const results = new Map();
|
|
22
|
+
host[method] = (path) => {
|
|
23
|
+
if (!results.has(path)) results.set(path, ts.sys[method](path));
|
|
24
|
+
return results.get(path);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function compilerOptions(config, path) {
|
|
28
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
29
|
+
config,
|
|
30
|
+
{ ...host, readDirectory: () => [] },
|
|
31
|
+
dirname(path),
|
|
32
|
+
undefined,
|
|
33
|
+
path,
|
|
34
|
+
);
|
|
35
|
+
return parsed.errors.some((error) => error.code !== 18003) ? null : parsed.options;
|
|
36
|
+
}
|
|
37
|
+
function options(filename) {
|
|
38
|
+
const directory = dirname(filename);
|
|
39
|
+
if (!configs.has(directory)) {
|
|
40
|
+
const path = ts.findConfigFile(directory, host.fileExists);
|
|
41
|
+
if (!configFiles.has(path)) {
|
|
42
|
+
const config = path && ts.readConfigFile(path, host.readFile);
|
|
43
|
+
configFiles.set(
|
|
44
|
+
path,
|
|
45
|
+
config?.error
|
|
46
|
+
? null
|
|
47
|
+
: config
|
|
48
|
+
? compilerOptions(config.config, path)
|
|
49
|
+
: { moduleResolution: ts.ModuleResolutionKind.Bundler },
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
configs.set(directory, configFiles.get(path));
|
|
53
|
+
}
|
|
54
|
+
return configs.get(directory);
|
|
55
|
+
}
|
|
56
|
+
function resolve(source, filename) {
|
|
57
|
+
if (!resolved.has(filename)) resolved.set(filename, new Map());
|
|
58
|
+
const imports = resolved.get(filename);
|
|
59
|
+
if (!imports.has(source)) {
|
|
60
|
+
const config = options(filename);
|
|
61
|
+
let target = null;
|
|
62
|
+
if (config && !config.noResolve) {
|
|
63
|
+
// Resolver caches must not mix paths/baseUrl from distinct tsconfigs.
|
|
64
|
+
if (!resolutionCaches.has(config))
|
|
65
|
+
resolutionCaches.set(
|
|
66
|
+
config,
|
|
67
|
+
ts.createModuleResolutionCache(process.cwd(), (path) => path, config),
|
|
68
|
+
);
|
|
69
|
+
target = ts.resolveModuleName(source, filename, config, host, resolutionCaches.get(config))
|
|
70
|
+
.resolvedModule?.resolvedFileName;
|
|
71
|
+
}
|
|
72
|
+
imports.set(source, target);
|
|
73
|
+
}
|
|
74
|
+
return imports.get(source);
|
|
75
|
+
}
|
|
76
|
+
function load(filename) {
|
|
77
|
+
if (!files.has(filename)) {
|
|
78
|
+
files.set(filename, null);
|
|
79
|
+
const text = host.readFile(filename);
|
|
80
|
+
try {
|
|
81
|
+
files.set(filename, parsedSource(filename, text, parser));
|
|
82
|
+
} catch {
|
|
83
|
+
// Missing, ambient, or unsupported source never establishes an awaiting contract.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return files.get(filename);
|
|
87
|
+
}
|
|
88
|
+
return { resolve, load };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { createSourceSession };
|