ESLint Plugin Encapsulation Broken By Language Property?
Understanding the Bug: Language Property Conflicts in ESLint Plugins
In the realm of JavaScript linting, ESLint stands as a cornerstone tool, ensuring code quality and consistency across projects. Plugins enhance ESLint's capabilities, but a peculiar issue arises when the language property interacts unexpectedly with plugin encapsulation. This article delves into this bug, exploring its causes, consequences, and potential solutions, offering valuable insights for both plugin developers and ESLint users.
The Environment
To set the stage, let's consider the environment where this issue surfaces:
- Node version: v25.2.1
- npm version: v11.6.2
- Local ESLint version: v9.39.1 (Currently used)
- Global ESLint version: Not found
- Operating System: win32 10.0.26200
- Parser: Default (Espree)
These details highlight the context in which the bug manifests, providing a reference point for replication and debugging.
The Problem: ESLint Configuration and Plugin Conflicts
The heart of the matter lies in how ESLint configurations merge, particularly when plugins introduce specific language settings. Consider the following configuration snippet:
import tseslint from 'typescript-eslint';
import { myPlugin } from './myPlugin.mjs';
export default [
{
languageOptions: {
parserOptions: {
projectService: true
}
}
},
...tseslint.configs.recommendedTypeChecked,
myPlugin.configs.recommended,
];
This configuration imports a custom plugin (myPlugin) alongside the recommended TypeScript ESLint configurations. The plugin itself defines configurations for specific file types:
// myPlugin.mjs
import json from '@eslint/json';
export const myPlugin = {
configs: {
recommended: {
files: ['package.json'],
language: 'json/json',
plugins: {
json
},
rules: {
'json/no-duplicate-keys': 'error'
}
}
}
};
Here, myPlugin targets package.json files, specifying the json/json language processor and a rule to prevent duplicate keys. The expectation is that ESLint would process package.json according to these JSON-specific settings. However, the reality is often different.
The Unexpected Outcome: Errors and Configuration Clashes
Instead of a smooth linting process, an error erupts:
Oops! Something went wrong! :(
ESLint: 9.39.1
Error: Error while loading rule '@typescript-eslint/await-thenable': You have used a rule which requires type information, but don't have parserOptions set to generate type information for this file. See https://typescript-eslint.io/getting-started/typed-linting for enabling linting with type information.
Parser: typescript-eslint/parser
Occurred while linting F:\dev\projects\eslint-config-merge-bug\package.json
at throwError (F:\dev\projects\eslint-config-merge-bug\node_modules\@typescript-eslint\utils\dist\eslint-utils\getParserServices.js:38:11)
at getParserServices (F:\dev\projects\eslint-config-merge-bug\node_modules\@typescript-eslint\utils\dist\eslint-utils\getParserServices.js:21:9)
at create (F:\dev\projects\eslint-config-merge-bug\node_modules\@typescript-eslint\eslint-plugin\dist\rules\await-thenable.js:63:55)
at Object.create (F:\dev\projects\eslint-config-merge-bug\node_modules\@typescript-eslint\utils\dist\eslint-utils\RuleCreator.js:31:20)
at createRuleListeners (F:\dev\projects\eslint-config-merge-bug\node_modules\eslint\lib\linter\linter.js:1019:15)
at F:\dev\projects\eslint-config-merge-bug\node_modules\eslint\lib\linter\linter.js:1151:7
at Array.forEach ()
at runRules (F:\dev\projects\eslint-config-merge-bug\node_modules\eslint\lib\linter\linter.js:1085:31)
at #flatVerifyWithoutProcessors (F:\dev\projects\eslint-config-merge-bug\node_modules\eslint\lib\linter\linter.js:2101:4)
at Linter._verifyWithFlatConfigArrayAndWithoutProcessors (F:\dev\projects\eslint-config-merge-bug\node_modules\eslint\lib\linter\linter.js:2189:43)
The error message reveals that a TypeScript-specific rule (@typescript-eslint/await-thenable) is being applied to package.json, a JSON file. This occurs because ESLint's configuration merging process doesn't isolate language-specific settings as expected.
The Root Cause: ESLint's Configuration Merging Mechanism
The core issue lies in ESLint's flat config merging strategy. Here's a breakdown:
- The plugin sets
language: 'json/json'forpackage.json, aiming to process it as a JSON file. - An external configuration (in this case, the TypeScript configuration) applies rules to all files, including
package.json. - ESLint merges these configurations, resulting in a combined setup:
- Language processor: JSON (from the plugin)
- Rules: TypeScript rules (from the external config)
- When ESLint executes these rules, the TypeScript rules fail because the JSON language processor doesn't provide the TypeScript Abstract Syntax Tree (AST) methods they expect.
This merging behavior fundamentally undermines plugin encapsulation, creating a scenario where plugin-specific settings are overridden by broader configurations.
The Seriousness of the Issue: Why Encapsulation Matters
This bug isn't merely a minor inconvenience; it represents a fundamental encapsulation problem with far-reaching consequences.
1. Plugins cannot protect their configuration
Plugin authors cannot guarantee that their files will be processed correctly. The language property, intended to isolate processing, fails to do so.
2. Silent config merging breaks expectations
The plugin explicitly sets language: 'json/json', but ESLint merges in incompatible rules from later configurations, leading to unexpected behavior.
3. No way to prevent it
Currently, there's no mechanism for a plugin to declare,