Skip to content

Commit 96663fb

Browse files
committed
fix(linter/plugins): do not call before hook if empty visitor (#14401)
If rule is defined using `createOnce`, and it returns an empty visitor (`before` / `after` hooks only), then don't run the hooks - the rule is a no-op. Reason this is important is forward-compatibility with an optimization we could make where we calculate on Rust side whether AST contains any nodes that rules act on, and if it doesn't, skip calling into JS entirely. In that case, the `before` hook wouldn't run on those files. We can't emulate that behavior entirely, but we can at least avoid users creating rules which *only* contain a `before` hook, and then finding that when we apply that optimization later on, it stops behaving as expected. i.e. we make the `before` hook never be called for a rule defined like this: ```js export default { createOnce(context) { return { before() { if (content.filename.endsWith('.tsx') { context.report({ message: 'No TSX!', node: context.sourceCode.ast }); } }, }; }, }; ```
1 parent 266b982 commit 96663fb

File tree

13 files changed

+42
-88
lines changed

13 files changed

+42
-88
lines changed

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ const registeredPluginPaths = new Set<string>();
6060
// Indexed by `ruleId`, which is passed to `lintFile`.
6161
export const registeredRules: RuleAndContext[] = [];
6262

63+
// `before` hook which makes rule never run.
64+
const neverRunBeforeHook: BeforeHook = () => false;
65+
6366
// Plugin details returned to Rust
6467
interface PluginDetails {
6568
// Plugin name
@@ -146,6 +149,19 @@ async function loadPluginImpl(path: string): Promise<PluginDetails> {
146149
beforeHook = conformHookFn(beforeHook, 'before');
147150
afterHook = conformHookFn(afterHook, 'after');
148151

152+
// If empty visitor, make this rule never run by substituting a `before` hook which always returns `false`.
153+
// This means the original `before` hook won't run either.
154+
//
155+
// Reason for doing this is:
156+
// In future, we may do a check on Rust side whether AST contains any nodes which rules act on,
157+
// and if not, skip calling into JS entirely. In that case, the `before` hook won't get called.
158+
// We can't emulate that behavior exactly, but we can at least emulate it in this simple case,
159+
// and prevent users defining rules with *only* a `before` hook, which they expect to run on every file.
160+
if (ObjectKeys(visitor).length === 0) {
161+
beforeHook = neverRunBeforeHook;
162+
afterHook = null;
163+
}
164+
149165
ruleAndContext = { rule, context, visitor, beforeHook, afterHook };
150166
} else {
151167
ruleAndContext = { rule, context, visitor: null, beforeHook: null, afterHook: null };

apps/oxlint/test/fixtures/createOnce/.oxlintrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"create-once-plugin/skip-run": "error",
77
"create-once-plugin/before-only": "error",
88
"create-once-plugin/after-only": "error",
9+
"create-once-plugin/only-hooks": "error",
910
"create-once-plugin/no-hooks": "error"
1011
}
1112
}

apps/oxlint/test/fixtures/createOnce/plugin.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,20 @@ const afterOnlyRule: Rule = {
117117
},
118118
};
119119

120+
const hooksOnlyRule: Rule = {
121+
createOnce(context) {
122+
return {
123+
// Neither hook should be called, because no AST node visitor functions
124+
before() {
125+
context.report({ message: 'before hook: should not be output', node: SPAN });
126+
},
127+
after() {
128+
context.report({ message: 'after hook: should not be output', node: SPAN });
129+
},
130+
};
131+
},
132+
};
133+
120134
const noHooksRule: Rule = {
121135
createOnce(context) {
122136
return {
@@ -139,6 +153,7 @@ const plugin: Plugin = {
139153
'skip-run': skipRunRule,
140154
'before-only': beforeOnlyRule,
141155
'after-only': afterOnlyRule,
156+
'only-hooks': hooksOnlyRule,
142157
'no-hooks': noHooksRule,
143158
},
144159
};

apps/oxlint/test/fixtures/custom_plugin_lint_after_hook_error/output.snap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
x Error running JS plugin.
77
| File path: <root>/apps/oxlint/test/fixtures/custom_plugin_lint_after_hook_error/files/index.js
88
| Error: Whoops!
9-
| at after (<root>/apps/oxlint/test/fixtures/custom_plugin_lint_after_hook_error/plugin.ts:12:19)
9+
| at after (<root>/apps/oxlint/test/fixtures/custom_plugin_lint_after_hook_error/plugin.ts:13:19)
1010
1111
Found 0 warnings and 1 error.
1212
Finished in Xms on 1 file using X threads.

apps/oxlint/test/fixtures/custom_plugin_lint_after_hook_error/plugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const plugin: Plugin = {
88
error: {
99
createOnce(_context) {
1010
return {
11+
Program(_program) {},
1112
after() {
1213
throw new Error('Whoops!');
1314
},

apps/oxlint/test/fixtures/custom_plugin_lint_before_hook_error/plugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const plugin: Plugin = {
1111
before() {
1212
throw new Error('Whoops!');
1313
},
14+
Program(_program) {},
1415
};
1516
},
1617
},

apps/oxlint/test/fixtures/definePlugin/output.snap.md

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,6 @@
4848
: ^
4949
`----
5050
51-
x define-plugin-plugin(create-once-hooks-only): before hook:
52-
| filename: files/1.js
53-
,-[files/1.js:1:1]
54-
1 | let a, b;
55-
: ^
56-
`----
57-
58-
x define-plugin-plugin(create-once-hooks-only): after hook:
59-
| filename: files/1.js
60-
,-[files/1.js:1:1]
61-
1 | let a, b;
62-
: ^
63-
`----
64-
6551
x define-plugin-plugin(create): ident visit fn "a":
6652
| filename: files/1.js
6753
,-[files/1.js:1:5]
@@ -186,20 +172,6 @@
186172
: ^
187173
`----
188174
189-
x define-plugin-plugin(create-once-hooks-only): before hook:
190-
| filename: files/2.js
191-
,-[files/2.js:1:1]
192-
1 | let c, d;
193-
: ^
194-
`----
195-
196-
x define-plugin-plugin(create-once-hooks-only): after hook:
197-
| filename: files/2.js
198-
,-[files/2.js:1:1]
199-
1 | let c, d;
200-
: ^
201-
`----
202-
203175
x define-plugin-plugin(create): ident visit fn "c":
204176
| filename: files/2.js
205177
,-[files/2.js:1:5]
@@ -286,7 +258,7 @@
286258
: ^
287259
`----
288260
289-
Found 0 warnings and 39 errors.
261+
Found 0 warnings and 35 errors.
290262
Finished in Xms on 2 files using X threads.
291263
```
292264

apps/oxlint/test/fixtures/definePlugin/plugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ const createOnceAfterOnlyRule: Rule = {
187187
const createOnceHooksOnlyRule: Rule = {
188188
createOnce(context) {
189189
return {
190+
// Neither hook should be called, because no AST node visitor functions
190191
before() {
191192
context.report({
192193
message: 'before hook:\n' +

apps/oxlint/test/fixtures/definePlugin_and_defineRule/output.snap.md

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,6 @@
4848
: ^
4949
`----
5050
51-
x define-plugin-and-rule-plugin(create-once-hooks-only): before hook:
52-
| filename: files/1.js
53-
,-[files/1.js:1:1]
54-
1 | let a, b;
55-
: ^
56-
`----
57-
58-
x define-plugin-and-rule-plugin(create-once-hooks-only): after hook:
59-
| filename: files/1.js
60-
,-[files/1.js:1:1]
61-
1 | let a, b;
62-
: ^
63-
`----
64-
6551
x define-plugin-and-rule-plugin(create): ident visit fn "a":
6652
| filename: files/1.js
6753
,-[files/1.js:1:5]
@@ -186,20 +172,6 @@
186172
: ^
187173
`----
188174
189-
x define-plugin-and-rule-plugin(create-once-hooks-only): before hook:
190-
| filename: files/2.js
191-
,-[files/2.js:1:1]
192-
1 | let c, d;
193-
: ^
194-
`----
195-
196-
x define-plugin-and-rule-plugin(create-once-hooks-only): after hook:
197-
| filename: files/2.js
198-
,-[files/2.js:1:1]
199-
1 | let c, d;
200-
: ^
201-
`----
202-
203175
x define-plugin-and-rule-plugin(create): ident visit fn "c":
204176
| filename: files/2.js
205177
,-[files/2.js:1:5]
@@ -286,7 +258,7 @@
286258
: ^
287259
`----
288260
289-
Found 0 warnings and 39 errors.
261+
Found 0 warnings and 35 errors.
290262
Finished in Xms on 2 files using X threads.
291263
```
292264

apps/oxlint/test/fixtures/definePlugin_and_defineRule/plugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ const createOnceAfterOnlyRule = defineRule({
186186
const createOnceHooksOnlyRule = defineRule({
187187
createOnce(context) {
188188
return {
189+
// Neither hook should be called, because no AST node visitor functions
189190
before() {
190191
context.report({
191192
message: 'before hook:\n' +

0 commit comments

Comments
 (0)