In Oxlint, when using `createOnce` API, we ensure that `after` hooks are always called for all rules even in the event of a rule throwing an error during AST traversal.
Uphold the same guarantee when a plugin using `createOnce` API runs in ESLint via `eslintCompatPlugin`, including in language server environments where the process lives on after an error is thrown.
We now make sure that:
1. `after` hooks are always run even if an error occurs during AST visitation.
2. `after` hooks for all rules are always run after *all* the plugin's rules complete AST visitation (not just after each individual rule finishes AST visitation).
These guarantees allow creating a reliable per-file cache shared between rules with a pattern like this:
```ts
let cache: Data | null = null;
let numRunningRules = 0;
function setupCache(context) {
if (cache === null) cache = new Data(context);
numRunningRules++;
}
function teardownCache() {
numRunningRules--;
if (numRunningRules === 0) cache = null;
}
const rule1 = {
createOnce(context) {
return {
before() {
setupCache(context);
},
Identifier(node) {
// Use `cache`
},
after() {
teardownCache();
},
};
},
};
const rule2 = {
// Same as above
};
const rule3 = {
// Same as above
};
```
We can build on this to create a dedicated API for the shared cache use case, and have it work reliably in ESLint as well as Oxlint. Related: #20700.
Fix a bug in `eslintCompatPlugin`.
`after` hooks should fire after all other visit functions, including CFG event handlers.
Previously `eslintCompatPlugin` added a `Program:exit` visit function to run the `after` hook, but it turns out this isn't the last to fire during AST visitation - last is the `onCodePathEnd` CFG event on `Program` node.
Fix this by running `after` hook in `onCodePathEnd` event handler instead. This simplifies the code somewhat as we no longer need to make sure that the visit function's key is a selector with higher specificity than all other `:exit` visit functions.
The downside is that the `onCodePathEnd` handler needs to check each time that the node it's called with is root `Program` node. But we make that check as cheap as possible by keeping a reference to `Program` node in a local var.
Fix an incompatibility with `eslint-plugin-jsdoc`. It relies on properties of `context` being values rather than getters:
https://github.com/gajus/eslint-plugin-jsdoc/blob/26276d428631a2625575607cba7875dab6797091/src/buildForbidRuleDefinition.js#L86-L103
When `options` property is a getter, this code throws an error because it attempts to define a property with both `value` and `get` in its descriptor.
This PR replaces the `id` and `options` getters with value properties.
The getters were there to prevent the `Context` object from being mutated by rule code, and allow the object to be frozen, as it is in ESLint. But each rule gets its own `Context` object, so if it chooses to mutate it, it only harms itself, and can't impact other rules/plugins. In ESLint, trying to alter the frozen `Context` object would be a hard error, so it's doubtful that any ESLint plugins do this anyway.
This is no real downside to this change for us, so we might as well make the change, and fix `eslint-plugin-jsdoc` and any other plugins which may depend on ESLint's behavior here.
Fix an edge case bug in conversion of a plugin using `createOnce` to an ESLint-compatible plugin.
If a rule's visitor has an `after` hook, it needs to run after all other visitor functions. Previously we achieved this by adding `after` hook as `Program:exit` visitor fn.
However, this will not work properly if the visitor has a selector which matches `Program` but has a higher specificity. e.g.:
```js
const rule = {
createOnce(context) {
return {
"Program[sourceType='module']:exit"(node) {
// This should run before the `after` hook
},
after() {
// This should run only at very end of walking AST
}
};
},
};
```
In this case `Program[sourceType='module']` has higher specificity than `Program:exit`, so if `after` hook is converted to `Program:exit`, then the `after` hook doesn't run last.
Fix this by creating a more specific selector for the `after` hook - in this case `Program[type][type]`. `Program` always has a `type` property, so the selector always matches `Program`, but it executes last due to its higher specificity.
Closes#18611.
Previously `definePlugin` and `defineRule` were doing double duty:
1. Providing type safety (standard `defineConfig` pattern).
2. Converting a plugin/rule to be ESLint compatible
Combining the two duties is probably confusing to users. Users are familiar with the `defineConfig` pattern and these functions generally don't do anything at runtime - just require a specific type as param and return the input untouched.
This PR adds a separate function `eslintCompatPlugin` which does the conversion to an ESLint-compatible plugin.
`definePlugin` and `defineRule` are now no-ops at runtime - they just pass through the plugin/rule unchanged.