Files
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

216 lines
6.5 KiB
TypeScript

import type { Node, Plugin, Rule } from "#oxlint/plugins";
const SPAN: Node = {
start: 0,
end: 0,
range: [0, 0],
loc: {
start: { line: 0, column: 0 },
end: { line: 0, column: 0 },
},
};
let createOnceCallCount = 0;
const alwaysRunRule: Rule = {
createOnce(context) {
createOnceCallCount++;
// `this` should be the rule object
// oxlint-disable-next-line typescript-eslint/no-this-alias
const topLevelThis = this;
// Available but `null`
const { id, options } = context;
// Check that these APIs throw here
const cwdError = tryCatch(() => context.cwd);
const getCwdError = tryCatch(() => context.getCwd());
const filenameError = tryCatch(() => context.filename);
const getFilenameError = tryCatch(() => context.getFilename());
const physicalFilenameError = tryCatch(() => context.physicalFilename);
const getPhysicalFilenameError = tryCatch(() => context.getPhysicalFilename());
const sourceCodeError = tryCatch(() => context.sourceCode);
const getSourceCodeError = tryCatch(() => context.getSourceCode());
const settingsError = tryCatch(() => context.settings);
const parserOptionsError = tryCatch(() => context.parserOptions);
const parserPathError = tryCatch(() => context.parserPath);
const reportError = tryCatch(() => context.report({ message: "oh no", node: SPAN }));
return {
before() {
context.report({ message: `createOnce: id: ${id}`, node: SPAN });
context.report({ message: `createOnce: options: ${JSON.stringify(options)}`, node: SPAN });
context.report({ message: `createOnce: call count: ${createOnceCallCount}`, node: SPAN });
context.report({
message: `createOnce: this === rule: ${topLevelThis === alwaysRunRule}`,
node: SPAN,
});
context.report({ message: `createOnce: cwd error: ${cwdError?.message}`, node: SPAN });
context.report({
message: `createOnce: getCwd() error: ${getCwdError?.message}`,
node: SPAN,
});
context.report({
message: `createOnce: filename error: ${filenameError?.message}`,
node: SPAN,
});
context.report({
message: `createOnce: getFilename() error: ${getFilenameError?.message}`,
node: SPAN,
});
context.report({
message: `createOnce: physicalFilename error: ${physicalFilenameError?.message}`,
node: SPAN,
});
context.report({
message: `createOnce: getPhysicalFilename() error: ${getPhysicalFilenameError?.message}`,
node: SPAN,
});
context.report({
message: `createOnce: sourceCode error: ${sourceCodeError?.message}`,
node: SPAN,
});
context.report({
message: `createOnce: getSourceCode() error: ${getSourceCodeError?.message}`,
node: SPAN,
});
context.report({
message: `createOnce: settings error: ${settingsError?.message}`,
node: SPAN,
});
context.report({
message: `createOnce: parserOptions error: ${parserOptionsError?.message}`,
node: SPAN,
});
context.report({
message: `createOnce: parserPath error: ${parserPathError?.message}`,
node: SPAN,
});
context.report({
message: `createOnce: report error: ${reportError?.message}`,
node: SPAN,
});
context.report({ message: `before hook: id: ${context.id}`, node: SPAN });
context.report({ message: `before hook: filename: ${context.filename}`, node: SPAN });
},
Identifier(node) {
context.report({
message: `ident visit fn "${node.name}": filename: ${context.filename}`,
node,
});
},
after() {
context.report({ message: `after hook: id: ${context.id}`, node: SPAN });
context.report({ message: `after hook: filename: ${context.filename}`, node: SPAN });
},
};
},
};
const skipRunRule: Rule = {
createOnce(context) {
return {
before() {
context.report({ message: `before hook: id: ${context.id}`, node: SPAN });
context.report({ message: `before hook: filename: ${context.filename}`, node: SPAN });
// Skip running this rule
return false;
},
Identifier(node) {
context.report({ message: `ident visit fn "${node.name}": should not be output`, node });
},
after() {
context.report({ message: "after hook: should not be output", node: SPAN });
},
};
},
};
const beforeOnlyRule: Rule = {
createOnce(context) {
return {
before() {
context.report({ message: `before hook: id: ${context.id}`, node: SPAN });
context.report({ message: `before hook: filename: ${context.filename}`, node: SPAN });
},
Identifier(node) {
context.report({
message: `ident visit fn "${node.name}": filename: ${context.filename}`,
node,
});
},
};
},
};
const afterOnlyRule: Rule = {
createOnce(context) {
return {
Identifier(node) {
context.report({
message: `ident visit fn "${node.name}": filename: ${context.filename}`,
node,
});
},
after() {
context.report({ message: `after hook: id: ${context.id}`, node: SPAN });
context.report({ message: `after hook: filename: ${context.filename}`, node: SPAN });
},
};
},
};
const hooksOnlyRule: Rule = {
createOnce(context) {
return {
// Neither hook should be called, because no AST node visitor functions
before() {
context.report({ message: "before hook: should not be output", node: SPAN });
},
after() {
context.report({ message: "after hook: should not be output", node: SPAN });
},
};
},
};
const noHooksRule: Rule = {
createOnce(context) {
return {
Identifier(node) {
context.report({
message: `ident visit fn "${node.name}": filename: ${context.filename}`,
node,
});
},
};
},
};
const plugin: Plugin = {
meta: {
name: "create-once-plugin",
},
rules: {
"always-run": alwaysRunRule,
"skip-run": skipRunRule,
"before-only": beforeOnlyRule,
"after-only": afterOnlyRule,
"only-hooks": hooksOnlyRule,
"no-hooks": noHooksRule,
},
};
export default plugin;
function tryCatch(fn: () => unknown): Error {
try {
fn();
} catch (err) {
return err as Error;
}
throw new Error("Expected function to throw");
}