Skip to content

Commit

Permalink
feat: Implement Language#normalizeLanguageOptions() (#19104)
Browse files Browse the repository at this point in the history
* feat: Implement Language#normalizeLanguageOptions()

fixes #19037

* Move normalizeOptions logic

* Fix FlatConfigArray tests
  • Loading branch information
nzakas authored Nov 15, 2024
1 parent 902e707 commit 01557ce
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 58 deletions.
1 change: 1 addition & 0 deletions docs/src/extend/languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
5 changes: 5 additions & 0 deletions lib/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
76 changes: 76 additions & 0 deletions lib/languages/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
//-----------------------------------------------------------------------------
Expand All @@ -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.
Expand Down
28 changes: 1 addition & 27 deletions lib/linter/linter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 01557ce

Please sign in to comment.