Fixing `defaultOptions` Conflict In ESLint Plugins
Hey guys! Today, we're diving deep into a common challenge faced when developing ESLint plugins, specifically the require-meta-default-options
rule and its potential conflicts with ESLintUtils.RuleCreator
. This issue often pops up when trying to adhere to best practices while ensuring your plugin is both robust and type-safe. Let's break down the problem, explore some failed attempts, and figure out the best way forward.
The Problem: Conflicting Expectations
So, you're building an awesome ESLint plugin, and you're following the patterns set by giants like typescript-eslint
. Makes sense, right? They've paved the way, and we want to build on their solid foundation. The typical structure, as seen in a typescript-eslint
rule, places defaultOptions
at the root level of the rule definition. Check out this example to see what I mean. This approach feels clean and organized.
But then comes eslint-plugin-eslint-plugin
, a fantastic tool for linting your ESLint plugins (meta, I know!). It flags the require-meta-default-options
rule, highlighting that your rules are failing. The catch? It expects defaultOptions
to live within the meta
level of your rule definition. This is where the head-scratching begins. How do we reconcile these conflicting expectations?
The core issue is that eslint-plugin-eslint-plugin
's require-meta-default-options
rule expects defaultOptions
within the meta
object, while tools like ESLintUtils.RuleCreator
(used extensively, including in typescript-eslint
) expect it at the root level. This creates a dilemma for plugin developers aiming for both best practices and adherence to linting rules.
Failed Attempt 1: Moving defaultOptions
to meta
Okay, so the first instinct might be to move defaultOptions
into the meta
object. Seems logical, right? Make eslint-plugin-eslint-plugin
happy. But hold on! TypeScript throws a wrench in the works. You get an error something like this:
Argument of type '{ name: string; meta: { type: "layout"; docs: { description: string; recommended: true; requiresTypeChecking: false; }; fixable: "whitespace"; schema: { type: "object"; properties: { maxLength: { type: "number"; description: string; }; }; additionalProperties: false; }[]; defaultOptions: [...]; messages: { ...; }; }...' is not assignable to parameter of type 'Readonly<RuleWithMetaAndName<Options, "incorrectlyFormatted", MyPluginDocs>>'.
Property 'defaultOptions' is missing in type '{ name: string; meta: { type: "layout"; docs: { description: string; recommended: true; requiresTypeChecking: false; }; fixable: "whitespace"; schema: { type: "object"; properties: { maxLength: { type: "number"; description: string; }; }; additionalProperties: false; }[]; defaultOptions: [...]; messages: { ...; }; }...' but required in type 'Readonly<RuleWithMetaAndName<Options, "incorrectlyFormatted", MyPluginDocs>>'.
Why? Because ESLintUtils.RuleCreator
expects defaultOptions
as a mandatory property at the root level. Moving it to meta
breaks the contract. So, that's a no-go.
When you attempt to move defaultOptions
to the meta
level, TypeScript's type checking, particularly when using ESLintUtils.RuleCreator
, throws an error. This is because ESLintUtils.RuleCreator
expects defaultOptions
as a top-level property, not nested within meta
. This approach, while seemingly aligning with eslint-plugin-eslint-plugin
's expectations, violates the requirements of the RuleCreator
utility.
Failed Attempt 2: Duplicating with a Variable
Alright, duplicating the same options in two places would violate the DRY (Don't Repeat Yourself) principle. We're programmers; we hate repetition! So, the next idea might be to store the options in a variable and reuse it:
const defaultOptions = [
{
someOption: 100,
},
] satisfies Options;
export const myRule = createRule<Options, MessageIds>({
name: "my-rule",
meta: {
// [snip]
defaultOptions,
},
// [snip]
defaultOptions
});
Clever, right? We're using a variable to keep things consistent. But... it still fails the require-meta-default-options
rule. Why? Because the rule isn't type-aware. It sees that defaultOptions
at the meta
level isn't a literal array; it's a variable. And that's a no-no in its book.
Employing a variable to store and reuse the defaultOptions
array, while a good practice for code maintainability, doesn't satisfy the require-meta-default-options
rule. The rule, in its current implementation, isn't type-aware and expects a literal array definition within the meta
object. This limitation prevents developers from using more sophisticated patterns for managing default options.
The Questions We Need to Answer
This whole situation raises some crucial questions:
- Where should
defaultOptions
live? Should it be exclusively at the root level, exclusively withinmeta
, or in both places for the