Skip to content

Commit 381c32b

Browse files
mdjermanovicnzakas
andauthored
feat: Allow languages to provide defaultLanguageOptions (#19003)
* feat: Default `languageOptions` for languages * don't pass `languageOptions` from CLI when not needed * update jsdoc * update RuleTester * add flat-config-array unit tests * add linter test with no language options * fix browser test * remove `languageOptions.parser` check from `RuleTester` * add RuleTester test with no language options * update docs * remove unnecessary check * use `??` instead of `||` Co-authored-by: Nicholas C. Zakas <[email protected]> * default `languageOptions` to an empty object --------- Co-authored-by: Nicholas C. Zakas <[email protected]>
1 parent 78836d4 commit 381c32b

File tree

11 files changed

+278
-65
lines changed

11 files changed

+278
-65
lines changed

docs/src/extend/languages.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ A basic `Language` object must implement the following:
8585
The following optional members allow you to customize how ESLint interacts with the object:
8686

8787
* `visitorKeys` - visitor keys that are specific to the AST or CST. This is used to optimize traversal of the AST or CST inside of ESLint. While not required, it is strongly recommended, especially for AST or CST formats that deviate significantly from ESTree format.
88+
* `defaultLanguageOptions` - default `languageOptions` when the language is used. User-specified `languageOptions` are merged with this object when calculating the config for the file being linted.
8889
* `matchesSelectorClass(className, node, ancestry)` - allows you to specify selector classes, such as `:expression`, that match more than one node. This method is called whenever an [esquery](https://github.com/estools/esquery) selector contains a `:` followed by an identifier.
8990

9091
See [`JSONLanguage`](https://github.com/eslint/json/blob/main/src/languages/json-language.js) as an example of a basic `Language` class.

lib/cli.js

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -146,24 +146,29 @@ async function translateOptions({
146146
overrideConfigFile = void 0;
147147
}
148148

149-
let globals = {};
149+
const languageOptions = {};
150150

151151
if (global) {
152-
globals = global.reduce((obj, name) => {
152+
languageOptions.globals = global.reduce((obj, name) => {
153153
if (name.endsWith(":true")) {
154154
obj[name.slice(0, -5)] = "writable";
155155
} else {
156156
obj[name] = "readonly";
157157
}
158158
return obj;
159-
}, globals);
159+
}, {});
160+
}
161+
162+
if (parserOptions) {
163+
languageOptions.parserOptions = parserOptions;
164+
}
165+
166+
if (parser) {
167+
languageOptions.parser = await importer.import(parser);
160168
}
161169

162170
overrideConfig = [{
163-
languageOptions: {
164-
globals,
165-
parserOptions: parserOptions || {}
166-
},
171+
...Object.keys(languageOptions).length > 0 ? { languageOptions } : {},
167172
rules: rule ? rule : {}
168173
}];
169174

@@ -175,10 +180,6 @@ async function translateOptions({
175180
};
176181
}
177182

178-
if (parser) {
179-
overrideConfig[0].languageOptions.parser = await importer.import(parser);
180-
}
181-
182183
if (plugin) {
183184
overrideConfig[0].plugins = await loadPlugins(importer, plugin);
184185
}

lib/config/config.js

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,6 @@ class Config {
188188
this.plugins = plugins;
189189
this.language = language;
190190

191-
if (languageOptions) {
192-
this.languageOptions = languageOptions;
193-
}
194-
195191
// Check language value
196192
const { pluginName: languagePluginName, objectName: localLanguageName } = splitPluginIdentifier(language);
197193

@@ -203,13 +199,20 @@ class Config {
203199

204200
this.language = plugins[languagePluginName].languages[localLanguageName];
205201

202+
if (this.language.defaultLanguageOptions ?? languageOptions) {
203+
this.languageOptions = flatConfigSchema.languageOptions.merge(
204+
this.language.defaultLanguageOptions,
205+
languageOptions
206+
);
207+
} else {
208+
this.languageOptions = {};
209+
}
210+
206211
// Validate language options
207-
if (this.languageOptions) {
208-
try {
209-
this.language.validateLanguageOptions(this.languageOptions);
210-
} catch (error) {
211-
throw new TypeError(`Key "languageOptions": ${error.message}`, { cause: error });
212-
}
212+
try {
213+
this.language.validateLanguageOptions(this.languageOptions);
214+
} catch (error) {
215+
throw new TypeError(`Key "languageOptions": ${error.message}`, { cause: error });
213216
}
214217

215218
// Check processor value

lib/config/default-config.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,6 @@ exports.defaultConfig = Object.freeze([
4242
}
4343
},
4444
language: "@/js",
45-
languageOptions: {
46-
sourceType: "module",
47-
ecmaVersion: "latest",
48-
parser: require("espree"),
49-
parserOptions: {}
50-
},
5145
linterOptions: {
5246
reportUnusedDisableDirectives: 1
5347
}

lib/languages/js/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
const { SourceCode } = require("./source-code");
1313
const createDebug = require("debug");
1414
const astUtils = require("../../shared/ast-utils");
15+
const espree = require("espree");
1516
const eslintScope = require("eslint-scope");
1617
const evk = require("eslint-visitor-keys");
1718
const { validateLanguageOptions } = require("./validate-language-options");
@@ -69,6 +70,13 @@ module.exports = {
6970
nodeTypeKey: "type",
7071
visitorKeys: evk.KEYS,
7172

73+
defaultLanguageOptions: {
74+
sourceType: "module",
75+
ecmaVersion: "latest",
76+
parser: espree,
77+
parserOptions: {}
78+
},
79+
7280
validateLanguageOptions,
7381

7482
/**

lib/linter/linter.js

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1643,29 +1643,26 @@ class Linter {
16431643
const options = normalizeVerifyOptions(providedOptions, config);
16441644
const languageOptions = config.languageOptions;
16451645

1646-
languageOptions.ecmaVersion = normalizeEcmaVersionForLanguageOptions(
1647-
languageOptions.ecmaVersion
1648-
);
1649-
1650-
// double check that there is a parser to avoid mysterious error messages
1651-
if (!languageOptions.parser) {
1652-
throw new TypeError(`No parser specified for ${options.filename}`);
1653-
}
1646+
if (config.language === jslang) {
1647+
languageOptions.ecmaVersion = normalizeEcmaVersionForLanguageOptions(
1648+
languageOptions.ecmaVersion
1649+
);
16541650

1655-
// Espree expects this information to be passed in
1656-
if (isEspree(languageOptions.parser)) {
1657-
const parserOptions = languageOptions.parserOptions;
1651+
// Espree expects this information to be passed in
1652+
if (isEspree(languageOptions.parser)) {
1653+
const parserOptions = languageOptions.parserOptions;
16581654

1659-
if (languageOptions.sourceType) {
1655+
if (languageOptions.sourceType) {
16601656

1661-
parserOptions.sourceType = languageOptions.sourceType;
1657+
parserOptions.sourceType = languageOptions.sourceType;
16621658

1663-
if (
1664-
parserOptions.sourceType === "module" &&
1665-
parserOptions.ecmaFeatures &&
1666-
parserOptions.ecmaFeatures.globalReturn
1667-
) {
1668-
parserOptions.ecmaFeatures.globalReturn = false;
1659+
if (
1660+
parserOptions.sourceType === "module" &&
1661+
parserOptions.ecmaFeatures &&
1662+
parserOptions.ecmaFeatures.globalReturn
1663+
) {
1664+
parserOptions.ecmaFeatures.globalReturn = false;
1665+
}
16691666
}
16701667
}
16711668
}

lib/rule-tester/rule-tester.js

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const parserSymbol = Symbol.for("eslint.RuleTester.parser");
3030
const { ConfigArraySymbol } = require("@eslint/config-array");
3131
const { isSerializable } = require("../shared/serialization");
3232

33+
const jslang = require("../languages/js");
3334
const { SourceCode } = require("../languages/js/source-code");
3435

3536
//------------------------------------------------------------------------------
@@ -619,10 +620,7 @@ class RuleTester {
619620
}
620621
}
621622
},
622-
language: defaultConfig[0].language,
623-
languageOptions: {
624-
...defaultConfig[0].languageOptions
625-
}
623+
language: defaultConfig[0].language
626624
},
627625
...defaultConfig.slice(1)
628626
];
@@ -673,7 +671,10 @@ class RuleTester {
673671
const calculatedConfig = proto[ConfigArraySymbol.finalizeConfig].apply(this, args);
674672

675673
// wrap the parser to catch start/end property access
676-
calculatedConfig.languageOptions.parser = wrapParser(calculatedConfig.languageOptions.parser);
674+
if (calculatedConfig.language === jslang) {
675+
calculatedConfig.languageOptions.parser = wrapParser(calculatedConfig.languageOptions.parser);
676+
}
677+
677678
return calculatedConfig;
678679
};
679680

@@ -694,17 +695,6 @@ class RuleTester {
694695
delete itemConfig[parameter];
695696
}
696697

697-
// wrap any parsers
698-
if (itemConfig.languageOptions && itemConfig.languageOptions.parser) {
699-
700-
const parser = itemConfig.languageOptions.parser;
701-
702-
if (parser && typeof parser !== "object") {
703-
throw new Error("Parser must be an object with a parse() or parseForESLint() method.");
704-
}
705-
706-
}
707-
708698
/*
709699
* Create the config object from the tester config and this item
710700
* specific configurations.

tests/lib/cli-engine/cli-engine.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -923,13 +923,15 @@ describe("CLIEngine", () => {
923923

924924
engine = new CLIEngine({
925925
parser: "esprima",
926-
useEslintrc: false
926+
useEslintrc: false,
927+
ignore: false
927928
});
928929

929-
const report = engine.executeOnFiles(["lib/cli.js"]);
930+
const report = engine.executeOnFiles(["tests/fixtures/simple-valid-project/foo.js"]);
930931

931932
assert.strictEqual(report.results.length, 1);
932933
assert.strictEqual(report.results[0].messages.length, 0);
934+
933935
assert.strictEqual(report.results[0].suppressedMessages.length, 0);
934936
});
935937

0 commit comments

Comments
 (0)