Skip to content

Commit c4812ec

Browse files
committed
fix(linter/plugins): reset visitor compilation state if error during compilation (#20166)
Fix a bug where internal state of visitor compilation could fail to be reset if an error is thrown during compilation, for example due to an invalid visitor being passed. If this happens, previously some of the visitor functions defined for one file could stick around, and be erroneously included in the compiled visitor for next file, leading to them firing on that 2nd file when they shouldn't. Fix that by resetting visitor compilation state in the `try` / `catch` which wraps the whole linting process.
1 parent 3dabb8e commit c4812ec

File tree

7 files changed

+105
-0
lines changed

7 files changed

+105
-0
lines changed

apps/oxlint/src-js/plugins/lint.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,13 @@ export function resetFile() {
274274
* in the correct initial state for linting the next file.
275275
*/
276276
export function resetStateAfterError() {
277+
// In case error occurred during visitor compilation, clear internal state of visitor compilation,
278+
// so no leftovers bleed into next file.
279+
// We could have a separate function to reset state which could be simpler and faster, but `resetStateAfterError`
280+
// should never be called - only happens when rules return an invalid visitor or malfunction.
281+
// So better to use the existing function, rather than bloat the package with more code which should never run.
282+
finalizeCompiledVisitor();
283+
277284
diagnostics.length = 0;
278285
ancestors.length = 0;
279286
resetFile();
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"jsPlugins": ["./plugin.ts"],
3+
"categories": { "correctness": "off" },
4+
"rules": {
5+
"error-plugin/error": "error"
6+
}
7+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
let x;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
let y;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"singleThread": true
3+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Exit code
2+
1
3+
4+
# stdout
5+
```
6+
x Error running JS plugin.
7+
| File path: <fixture>/files/1.js
8+
| TypeError: 'Program' property of visitor object is not a function
9+
10+
x error-plugin(error): Identifier in 2nd file: y
11+
,-[files/2.js:1:5]
12+
1 | let y;
13+
: ^
14+
`----
15+
16+
Found 0 warnings and 2 errors.
17+
Finished in Xms on 2 files with 1 rules using X threads.
18+
```
19+
20+
# stderr
21+
```
22+
```
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import assert from "node:assert";
2+
3+
import type { Plugin, Rule } from "#oxlint/plugins";
4+
5+
// Aim of this test is:
6+
//
7+
// 1. Check that errors thrown during visitor compilation are handled correctly and shown to user as diagnostics.
8+
// 2. Check that global state is reset after an error during visiting a file, so it's in correct initial state
9+
// when Oxlint starts linting the next file.
10+
//
11+
// The 2nd is tricky to test because usually the order Oxlint lints files in is non-deterministic.
12+
// To make this test deterministic, we run it with `oxlint --threads 1`
13+
// (`options.json` file for this fixture contains `"singleThread": true`).
14+
// This guarantees that `1.js` is linted before `2.js`.
15+
//
16+
// This rule throws an error during visitor compilation when linting 1st file. If global state is not reset properly
17+
// before linting 2nd file, `Identifier` visitor for 1st file will not be cleaned up and will fire on 2nd file.
18+
19+
let fileIndex = 0;
20+
21+
const rule: Rule = {
22+
create(context) {
23+
const isFirstFileLinted = fileIndex === 0;
24+
fileIndex++;
25+
26+
// Check the order files get linted in is what we expect
27+
const isFirstFile = context.filename.endsWith("1.js");
28+
assert(isFirstFile === isFirstFileLinted);
29+
30+
if (isFirstFile) {
31+
return {
32+
Identifier(node) {
33+
context.report({
34+
message: `Identifier in 1st file: ${node.name}`,
35+
node,
36+
});
37+
},
38+
39+
// Illegal value, causes error during visitor compilation
40+
Program: 123 as any,
41+
};
42+
}
43+
44+
return {
45+
Identifier(node) {
46+
context.report({
47+
message: `Identifier in 2nd file: ${node.name}`,
48+
node,
49+
});
50+
},
51+
};
52+
},
53+
};
54+
55+
const plugin: Plugin = {
56+
meta: {
57+
name: "error-plugin",
58+
},
59+
rules: {
60+
error: rule,
61+
},
62+
};
63+
64+
export default plugin;

0 commit comments

Comments
 (0)