diff --git a/docs/src/extend/languages.md b/docs/src/extend/languages.md index cebb20dde670..2b993d575b4f 100644 --- a/docs/src/extend/languages.md +++ b/docs/src/extend/languages.md @@ -87,6 +87,7 @@ The following optional members allow you to customize how ESLint interacts with * `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. * `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. * `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. +* `normalizeLanguageOptions(languageOptions)` - takes a validated language options object and normalizes its values. This is helpful for backwards compatibility when language options properties change. See [`JSONLanguage`](https://github.com/eslint/json/blob/main/src/languages/json-language.js) as an example of a basic `Language` class. diff --git a/lib/config/config.js b/lib/config/config.js index 0bbb0a6e6bbf..009d09fa955e 100644 --- a/lib/config/config.js +++ b/lib/config/config.js @@ -195,6 +195,11 @@ class Config { throw new TypeError(`Key "languageOptions": ${error.message}`, { cause: error }); } + // Normalize language options if necessary + if (this.language.normalizeLanguageOptions) { + this.languageOptions = this.language.normalizeLanguageOptions(this.languageOptions); + } + // Check processor value if (processor) { this.processor = processor; diff --git a/lib/languages/js/index.js b/lib/languages/js/index.js index 5f53b81fc23c..98d72a54c727 100644 --- a/lib/languages/js/index.js +++ b/lib/languages/js/index.js @@ -16,6 +16,7 @@ const espree = require("espree"); const eslintScope = require("eslint-scope"); const evk = require("eslint-visitor-keys"); const { validateLanguageOptions } = require("./validate-language-options"); +const { LATEST_ECMA_VERSION } = require("../../../conf/ecma-version"); //----------------------------------------------------------------------------- // Type Definitions @@ -31,6 +32,7 @@ const { validateLanguageOptions } = require("./validate-language-options"); const debug = createDebug("eslint:languages:js"); const DEFAULT_ECMA_VERSION = 5; +const parserSymbol = Symbol.for("eslint.RuleTester.parser"); /** * Analyze scope of the given AST. @@ -55,6 +57,47 @@ function analyzeScope(ast, languageOptions, visitorKeys) { }); } +/** + * Determines if a given object is Espree. + * @param {Object} parser The parser to check. + * @returns {boolean} True if the parser is Espree or false if not. + */ +function isEspree(parser) { + return !!(parser === espree || parser[parserSymbol] === espree); +} + +/** + * Normalize ECMAScript version from the initial config into languageOptions (year) + * format. + * @param {any} [ecmaVersion] ECMAScript version from the initial config + * @returns {number} normalized ECMAScript version + */ +function normalizeEcmaVersionForLanguageOptions(ecmaVersion) { + + switch (ecmaVersion) { + case 3: + return 3; + + // void 0 = no ecmaVersion specified so use the default + case 5: + case void 0: + return 5; + + default: + if (typeof ecmaVersion === "number") { + return ecmaVersion >= 2015 ? ecmaVersion : ecmaVersion + 2009; + } + } + + /* + * We default to the latest supported ecmaVersion for everything else. + * Remember, this is for languageOptions.ecmaVersion, which sets the version + * that is used for a number of processes inside of ESLint. It's normally + * safe to assume people want the latest unless otherwise specified. + */ + return LATEST_ECMA_VERSION; +} + //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- @@ -79,6 +122,39 @@ module.exports = { validateLanguageOptions, + /** + * Normalizes the language options. + * @param {Object} languageOptions The language options to normalize. + * @returns {Object} The normalized language options. + */ + normalizeLanguageOptions(languageOptions) { + + languageOptions.ecmaVersion = normalizeEcmaVersionForLanguageOptions( + languageOptions.ecmaVersion + ); + + // Espree expects this information to be passed in + if (isEspree(languageOptions.parser)) { + const parserOptions = languageOptions.parserOptions; + + if (languageOptions.sourceType) { + + parserOptions.sourceType = languageOptions.sourceType; + + if ( + parserOptions.sourceType === "module" && + parserOptions.ecmaFeatures && + parserOptions.ecmaFeatures.globalReturn + ) { + parserOptions.ecmaFeatures.globalReturn = false; + } + } + } + + return languageOptions; + + }, + /** * Determines if a given node matches a given selector class. * @param {string} className The class name to check. diff --git a/lib/linter/linter.js b/lib/linter/linter.js index be3c259e19cc..007fcf459058 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -1655,34 +1655,8 @@ class Linter { const slots = internalSlotsMap.get(this); const config = providedConfig || {}; + const { settings = {}, languageOptions } = config; const options = normalizeVerifyOptions(providedOptions, config); - const languageOptions = config.languageOptions; - - if (config.language === jslang) { - languageOptions.ecmaVersion = normalizeEcmaVersionForLanguageOptions( - languageOptions.ecmaVersion - ); - - // Espree expects this information to be passed in - if (isEspree(languageOptions.parser)) { - const parserOptions = languageOptions.parserOptions; - - if (languageOptions.sourceType) { - - parserOptions.sourceType = languageOptions.sourceType; - - if ( - parserOptions.sourceType === "module" && - parserOptions.ecmaFeatures && - parserOptions.ecmaFeatures.globalReturn - ) { - parserOptions.ecmaFeatures.globalReturn = false; - } - } - } - } - - const settings = config.settings || {}; if (!slots.lastSourceCode) { let t; diff --git a/tests/lib/config/flat-config-array.js b/tests/lib/config/flat-config-array.js index 2218c8d3779d..5619ee17b9aa 100644 --- a/tests/lib/config/flat-config-array.js +++ b/tests/lib/config/flat-config-array.js @@ -14,6 +14,7 @@ const assert = require("chai").assert; const stringify = require("json-stable-stringify-without-jsonify"); const espree = require("espree"); const jslang = require("../../../lib/languages/js"); +const { LATEST_ECMA_VERSION } = require("../../../conf/ecma-version"); //----------------------------------------------------------------------------- // Helpers @@ -187,7 +188,7 @@ async function assertMergedResult(values, result) { } if (!result.languageOptions) { - result.languageOptions = jslang.defaultLanguageOptions; + result.languageOptions = jslang.normalizeLanguageOptions(jslang.defaultLanguageOptions); } assert.deepStrictEqual(config, result); @@ -282,10 +283,13 @@ describe("FlatConfigArray", () => { plugins: ["@", "a", "b"], language: "@/js", languageOptions: { - ecmaVersion: "latest", + ecmaVersion: LATEST_ECMA_VERSION, sourceType: "module", parser: `espree@${espree.version}`, - parserOptions: {} + parserOptions: { + sourceType: "module" + } + }, linterOptions: { reportUnusedDisableDirectives: 1 @@ -318,10 +322,12 @@ describe("FlatConfigArray", () => { plugins: ["@", "a", "b:b-plugin@2.3.1"], language: "@/js", languageOptions: { - ecmaVersion: "latest", + ecmaVersion: LATEST_ECMA_VERSION, sourceType: "module", parser: `espree@${espree.version}`, - parserOptions: {} + parserOptions: { + sourceType: "module" + } }, linterOptions: { reportUnusedDisableDirectives: 1 @@ -356,10 +362,12 @@ describe("FlatConfigArray", () => { plugins: ["@", "a", "b:b-plugin@2.3.1"], language: "@/js", languageOptions: { - ecmaVersion: "latest", + ecmaVersion: LATEST_ECMA_VERSION, sourceType: "module", parser: `espree@${espree.version}`, - parserOptions: {} + parserOptions: { + sourceType: "module" + } }, linterOptions: { reportUnusedDisableDirectives: 1 @@ -390,10 +398,12 @@ describe("FlatConfigArray", () => { plugins: ["@"], language: "@/js", languageOptions: { - ecmaVersion: "latest", + ecmaVersion: LATEST_ECMA_VERSION, sourceType: "module", parser: `espree@${espree.version}`, - parserOptions: {}, + parserOptions: { + sourceType: "module" + }, globals: { name: "off" } @@ -530,7 +540,7 @@ describe("FlatConfigArray", () => { assert.deepStrictEqual(config.toJSON(), { language: "@/js", languageOptions: { - ecmaVersion: "latest", + ecmaVersion: LATEST_ECMA_VERSION, parser: "custom-parser", parserOptions: {}, sourceType: "module" @@ -565,7 +575,7 @@ describe("FlatConfigArray", () => { assert.deepStrictEqual(config.toJSON(), { language: "@/js", languageOptions: { - ecmaVersion: "latest", + ecmaVersion: LATEST_ECMA_VERSION, parser: "custom-parser@0.1.0", parserOptions: {}, sourceType: "module" @@ -600,7 +610,7 @@ describe("FlatConfigArray", () => { assert.deepStrictEqual(config.toJSON(), { language: "@/js", languageOptions: { - ecmaVersion: "latest", + ecmaVersion: LATEST_ECMA_VERSION, parser: "custom-parser@0.1.0", parserOptions: {}, sourceType: "module" @@ -633,7 +643,7 @@ describe("FlatConfigArray", () => { assert.deepStrictEqual(config.toJSON(), { language: "@/js", languageOptions: { - ecmaVersion: "latest", + ecmaVersion: LATEST_ECMA_VERSION, parser: "custom-parser@0.1.0", parserOptions: {}, sourceType: "module" @@ -706,9 +716,11 @@ describe("FlatConfigArray", () => { assert.deepStrictEqual(config.toJSON(), { language: "@/js", languageOptions: { - ecmaVersion: "latest", + ecmaVersion: LATEST_ECMA_VERSION, parser: `espree@${espree.version}`, - parserOptions: {}, + parserOptions: { + sourceType: "module" + }, sourceType: "module" }, linterOptions: { @@ -737,9 +749,11 @@ describe("FlatConfigArray", () => { assert.deepStrictEqual(config.toJSON(), { language: "@/js", languageOptions: { - ecmaVersion: "latest", + ecmaVersion: LATEST_ECMA_VERSION, parser: `espree@${espree.version}`, - parserOptions: {}, + parserOptions: { + sourceType: "module" + }, sourceType: "module" }, linterOptions: { @@ -772,9 +786,11 @@ describe("FlatConfigArray", () => { assert.deepStrictEqual(config.toJSON(), { language: "@/js", languageOptions: { - ecmaVersion: "latest", + ecmaVersion: LATEST_ECMA_VERSION, parser: `espree@${espree.version}`, - parserOptions: {}, + parserOptions: { + sourceType: "module" + }, sourceType: "module" }, linterOptions: { @@ -805,9 +821,11 @@ describe("FlatConfigArray", () => { assert.deepStrictEqual(config.toJSON(), { language: "@/js", languageOptions: { - ecmaVersion: "latest", + ecmaVersion: LATEST_ECMA_VERSION, parser: `espree@${espree.version}`, - parserOptions: {}, + parserOptions: { + sourceType: "module" + }, sourceType: "module" }, linterOptions: { @@ -1446,7 +1464,10 @@ describe("FlatConfigArray", () => { languageOptions: { ...jslang.defaultLanguageOptions, ecmaVersion: 2019, - sourceType: "commonjs" + sourceType: "commonjs", + parserOptions: { + sourceType: "commonjs" + } } })); @@ -1658,7 +1679,10 @@ describe("FlatConfigArray", () => { language: jslang, languageOptions: { ...jslang.defaultLanguageOptions, - sourceType: "script" + sourceType: "script", + parserOptions: { + sourceType: "script" + } } })); @@ -1676,7 +1700,10 @@ describe("FlatConfigArray", () => { language: jslang, languageOptions: { ...jslang.defaultLanguageOptions, - sourceType: "script" + sourceType: "script", + parserOptions: { + sourceType: "script" + } } })); @@ -2058,7 +2085,8 @@ describe("FlatConfigArray", () => { ...jslang.defaultLanguageOptions, parserOptions: { foo: "whatever", - bar: "baz" + bar: "baz", + sourceType: "module" } } })); @@ -2091,8 +2119,9 @@ describe("FlatConfigArray", () => { parserOptions: { ecmaFeatures: { jsx: true, - globalReturn: true - } + globalReturn: false + }, + sourceType: "module" } } })); @@ -2122,7 +2151,8 @@ describe("FlatConfigArray", () => { parserOptions: { ecmaFeatures: { jsx: true - } + }, + sourceType: "module" } } })); @@ -2149,7 +2179,8 @@ describe("FlatConfigArray", () => { languageOptions: { ...jslang.defaultLanguageOptions, parserOptions: { - foo: "bar" + foo: "bar", + sourceType: "module" } } })); @@ -2171,7 +2202,8 @@ describe("FlatConfigArray", () => { languageOptions: { ...jslang.defaultLanguageOptions, parserOptions: { - foo: "whatever" + foo: "whatever", + sourceType: "module" } } })); @@ -2194,7 +2226,8 @@ describe("FlatConfigArray", () => { languageOptions: { ...jslang.defaultLanguageOptions, parserOptions: { - foo: "bar" + foo: "bar", + sourceType: "module" } } }));