Commit Graph

5 Commits

Author SHA1 Message Date
overlookmotel 0aa1ff0fcb fix(linter/plugins): ensure after hook is always called in ESLint compat mode (#20721)
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.
2026-03-25 01:02:05 +00:00
overlookmotel 31145a9c6d fix(linter/plugins): fire after hook after CFG events in ESLint compat (#20720)
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.
2026-03-25 01:02:05 +00:00
overlookmotel 0861d9a5c8 fix(linter/plugins): remove getters from Context (#20115)
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.
2026-03-08 17:27:52 +00:00
overlookmotel d64bfdde4b fix(linter/plugins): ensure after hook always runs last in rule converted for ESLint (#18904)
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.
2026-02-03 20:28:17 +00:00
overlookmotel 88f30e0b4a feat(linter/plugins): move eslint compatible plugin conversion to eslintCompatPlugin function (#18791)
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.
2026-02-01 12:37:48 +00:00