From 7ed74f7011411053749a04c365cf65489189e7be Mon Sep 17 00:00:00 2001 From: DoZerg Date: Wed, 16 Feb 2022 22:23:49 +0000 Subject: [PATCH 1/2] feat: Avoid dirname for built-in configs. Load eslint:recommended and eslint:all configs via import instead file paths. Fixes: https://github.com/eslint/eslint/issues/15575 --- conf/eslint-all.cjs | 12 - conf/eslint-recommended.cjs | 12 - lib/cascading-config-array-factory.js | 23 +- lib/config-array-factory.js | 50 +- lib/flat-compat.js | 11 +- tests/lib/cascading-config-array-factory.js | 3255 +++++++++++++------ tests/lib/config-array-factory.js | 1352 +++++--- 7 files changed, 3276 insertions(+), 1439 deletions(-) delete mode 100644 conf/eslint-all.cjs delete mode 100644 conf/eslint-recommended.cjs diff --git a/conf/eslint-all.cjs b/conf/eslint-all.cjs deleted file mode 100644 index 859811c8..00000000 --- a/conf/eslint-all.cjs +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @fileoverview Stub eslint:all config - * @author Nicholas C. Zakas - */ - -"use strict"; - -module.exports = { - settings: { - "eslint:all": true - } -}; diff --git a/conf/eslint-recommended.cjs b/conf/eslint-recommended.cjs deleted file mode 100644 index 96300919..00000000 --- a/conf/eslint-recommended.cjs +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @fileoverview Stub eslint:recommended config - * @author Nicholas C. Zakas - */ - -"use strict"; - -module.exports = { - settings: { - "eslint:recommended": true - } -}; diff --git a/lib/cascading-config-array-factory.js b/lib/cascading-config-array-factory.js index 553ca0a9..4b575c2d 100644 --- a/lib/cascading-config-array-factory.js +++ b/lib/cascading-config-array-factory.js @@ -22,13 +22,18 @@ // Requirements //------------------------------------------------------------------------------ +import debugOrig from "debug"; import os from "os"; import path from "path"; + +import { ConfigArrayFactory } from "./config-array-factory.js"; +import { + ConfigArray, + ConfigDependency, + IgnorePattern +} from "./config-array/index.js"; import ConfigValidator from "./shared/config-validator.js"; import { emitDeprecationWarning } from "./shared/deprecation-warnings.js"; -import { ConfigArrayFactory } from "./config-array-factory.js"; -import { ConfigArray, ConfigDependency, IgnorePattern } from "./config-array/index.js"; -import debugOrig from "debug"; const debug = debugOrig("eslintrc:cascading-config-array-factory"); @@ -57,7 +62,9 @@ const debug = debugOrig("eslintrc:cascading-config-array-factory"); * @property {Map} builtInRules The rules that are built in to ESLint. * @property {Object} [resolver=ModuleResolver] The module resolver object. * @property {string} eslintAllPath The path to the definitions for eslint:all. + * @property {Function} getEslintAllConfig Returns the config data for eslint:all. * @property {string} eslintRecommendedPath The path to the definitions for eslint:recommended. + * @property {Function} getEslintRecommendedConfig Returns the config data for eslint:recommended. */ /** @@ -78,7 +85,9 @@ const debug = debugOrig("eslintrc:cascading-config-array-factory"); * @property {Map} builtInRules The rules that are built in to ESLint. * @property {Object} [resolver=ModuleResolver] The module resolver object. * @property {string} eslintAllPath The path to the definitions for eslint:all. + * @property {Function} getEslintAllConfig Returns the config data for eslint:all. * @property {string} eslintRecommendedPath The path to the definitions for eslint:recommended. + * @property {Function} getEslintRecommendedConfig Returns the config data for eslint:recommended. */ /** @type {WeakMap} */ @@ -219,7 +228,9 @@ class CascadingConfigArrayFactory { loadRules, resolver, eslintRecommendedPath, - eslintAllPath + getEslintRecommendedConfig, + eslintAllPath, + getEslintAllConfig } = {}) { const configArrayFactory = new ConfigArrayFactory({ additionalPluginPool, @@ -228,7 +239,9 @@ class CascadingConfigArrayFactory { builtInRules, resolver, eslintRecommendedPath, - eslintAllPath + getEslintRecommendedConfig, + eslintAllPath, + getEslintAllConfig }); internalSlotsMap.set(this, { diff --git a/lib/config-array-factory.js b/lib/config-array-factory.js index b571e2f7..e1a67961 100644 --- a/lib/config-array-factory.js +++ b/lib/config-array-factory.js @@ -38,22 +38,23 @@ // Requirements //------------------------------------------------------------------------------ +import debugOrig from "debug"; import fs from "fs"; -import path from "path"; import importFresh from "import-fresh"; +import { createRequire } from "module"; +import path from "path"; import stripComments from "strip-json-comments"; -import ConfigValidator from "./shared/config-validator.js"; -import * as naming from "./shared/naming.js"; -import * as ModuleResolver from "./shared/relative-module-resolver.js"; + import { ConfigArray, ConfigDependency, IgnorePattern, OverrideTester } from "./config-array/index.js"; -import debugOrig from "debug"; +import ConfigValidator from "./shared/config-validator.js"; +import * as naming from "./shared/naming.js"; +import * as ModuleResolver from "./shared/relative-module-resolver.js"; -import { createRequire } from "module"; const require = createRequire(import.meta.url); const debug = debugOrig("eslintrc:config-array-factory"); @@ -90,7 +91,9 @@ const configFilenames = [ * @property {Map} builtInRules The rules that are built in to ESLint. * @property {Object} [resolver=ModuleResolver] The module resolver object. * @property {string} eslintAllPath The path to the definitions for eslint:all. + * @property {Function} getEslintAllConfig Returns the config data for eslint:all. * @property {string} eslintRecommendedPath The path to the definitions for eslint:recommended. + * @property {Function} getEslintRecommendedConfig Returns the config data for eslint:recommended. */ /** @@ -101,7 +104,9 @@ const configFilenames = [ * @property {Map} builtInRules The rules that are built in to ESLint. * @property {Object} [resolver=ModuleResolver] The module resolver object. * @property {string} eslintAllPath The path to the definitions for eslint:all. + * @property {Function} getEslintAllConfig Returns the config data for eslint:all. * @property {string} eslintRecommendedPath The path to the definitions for eslint:recommended. + * @property {Function} getEslintRecommendedConfig Returns the config data for eslint:recommended. */ /** @@ -428,7 +433,9 @@ class ConfigArrayFactory { builtInRules, resolver = ModuleResolver, eslintAllPath, - eslintRecommendedPath + getEslintAllConfig, + eslintRecommendedPath, + getEslintRecommendedConfig } = {}) { internalSlotsMap.set(this, { additionalPluginPool, @@ -439,7 +446,9 @@ class ConfigArrayFactory { builtInRules, resolver, eslintAllPath, - eslintRecommendedPath + getEslintAllConfig, + eslintRecommendedPath, + getEslintRecommendedConfig }); } @@ -797,20 +806,35 @@ class ConfigArrayFactory { * @private */ _loadExtendedBuiltInConfig(extendName, ctx) { - const { eslintAllPath, eslintRecommendedPath } = internalSlotsMap.get(this); + const { + eslintAllPath, + getEslintAllConfig, + eslintRecommendedPath, + getEslintRecommendedConfig + } = internalSlotsMap.get(this); if (extendName === "eslint:recommended") { + const name = `${ctx.name} » ${extendName}`; + + if (getEslintRecommendedConfig) { + return this._normalizeConfigData(getEslintRecommendedConfig(), { ...ctx, name }); + } return this._loadConfigData({ ...ctx, - filePath: eslintRecommendedPath, - name: `${ctx.name} » ${extendName}` + name, + filePath: eslintRecommendedPath }); } if (extendName === "eslint:all") { + const name = `${ctx.name} » ${extendName}`; + + if (getEslintAllConfig) { + return this._normalizeConfigData(getEslintAllConfig(), { ...ctx, name }); + } return this._loadConfigData({ ...ctx, - filePath: eslintAllPath, - name: `${ctx.name} » ${extendName}` + name, + filePath: eslintAllPath }); } diff --git a/lib/flat-compat.js b/lib/flat-compat.js index 7fa111d3..8df15a53 100644 --- a/lib/flat-compat.js +++ b/lib/flat-compat.js @@ -7,14 +7,11 @@ // Requirements //----------------------------------------------------------------------------- -import path from "path"; -import { fileURLToPath } from "url"; import createDebug from "debug"; +import path from "path"; -import { ConfigArrayFactory } from "./config-array-factory.js"; import environments from "../conf/environments.js"; - -const dirname = path.dirname(fileURLToPath(import.meta.url)); +import { ConfigArrayFactory } from "./config-array-factory.js"; //----------------------------------------------------------------------------- // Helpers @@ -225,8 +222,8 @@ class FlatCompat { this[cafactory] = new ConfigArrayFactory({ cwd: baseDirectory, resolvePluginsRelativeTo, - eslintAllPath: path.resolve(dirname, "../conf/eslint-all.cjs"), - eslintRecommendedPath: path.resolve(dirname, "../conf/eslint-recommended.cjs") + getEslintAllConfig: () => ({ settings: { "eslint:all": true } }), + getEslintRecommendedConfig: () => ({ settings: { "eslint:recommended": true } }) }); } diff --git a/tests/lib/cascading-config-array-factory.js b/tests/lib/cascading-config-array-factory.js index 64b70395..7250f9a8 100644 --- a/tests/lib/cascading-config-array-factory.js +++ b/tests/lib/cascading-config-array-factory.js @@ -7,18 +7,21 @@ // Requirements //----------------------------------------------------------------------------- +import { assert } from "chai"; import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; +import { createRequire } from "module"; import os from "os"; -import { assert } from "chai"; +import path from "path"; import sh from "shelljs"; import sinon from "sinon"; import systemTempDir from "temp-dir"; +import { fileURLToPath } from "url"; import { Legacy } from "../../lib/index.js"; import { createCustomTeardown } from "../_utils/index.js"; +const require = createRequire(import.meta.url); + const dirname = path.dirname(fileURLToPath(import.meta.url)); const { @@ -41,6 +44,22 @@ const cwdIgnorePatterns = new ConfigArrayFactory() const eslintAllPath = path.resolve(dirname, "../fixtures/eslint-all.cjs"); const eslintRecommendedPath = path.resolve(dirname, "../fixtures/eslint-recommended.cjs"); +/** + * Return config data for built-in eslint:all. + * @returns {ConfigData} Config data + */ +function getEslintAllConfig() { + return require("../fixtures/eslint-all.cjs"); +} + +/** + * Return config data for built-in eslint:recommended. + * @returns {ConfigData} Config data + */ +function getEslintRecommendedConfig() { + return require("../fixtures/eslint-recommended.cjs"); +} + //----------------------------------------------------------------------------- // Tests //----------------------------------------------------------------------------- @@ -528,7 +547,6 @@ describe("CascadingConfigArrayFactory", () => { // This group moved from 'tests/lib/config.js' when refactoring to keep the cumulated test cases. describe("with 'tests/fixtures/config-hierarchy' files", () => { - let fixtureDir; // hack to avoid needing to hand-rewrite file-structure.json const DIRECTORY_CONFIG_HIERARCHY = (() => { @@ -561,16 +579,6 @@ describe("CascadingConfigArrayFactory", () => { return flattened; })(); - /** - * Returns the path inside of the fixture directory. - * @param {...string} args file path segments. - * @returns {string} The path inside the fixture directory. - * @private - */ - function getFixturePath(...args) { - return path.join(fixtureDir, "config-hierarchy", ...args); - } - /** * Mocks the current user's home path * @param {string} fakeUserHomePath fake user's home path @@ -624,1217 +632,2414 @@ describe("CascadingConfigArrayFactory", () => { .toCompatibleObjectAsConfigFileContent(); } - // copy into clean area so as not to get "infected" by this project's .eslintrc files - before(function() { + describe("with eslint built-in config paths", () => { + let fixtureDir; - /* - * GitHub Actions Windows and macOS runners occasionally exhibit - * extremely slow filesystem operations, during which copying fixtures - * exceeds the default test timeout, so raise it just for this hook. - * Mocha uses `this` to set timeouts on an individual hook level. + /** + * Returns the path inside of the fixture directory. + * @param {...string} args file path segments. + * @returns {string} The path inside the fixture directory. + * @private */ - this.timeout(60 * 1000); // eslint-disable-line no-invalid-this - - fixtureDir = `${systemTempDir}/eslint/fixtures`; - sh.mkdir("-p", fixtureDir); - sh.cp("-r", "./tests/fixtures/config-hierarchy", fixtureDir); - sh.cp("-r", "./tests/fixtures/rules", fixtureDir); - }); + function getFixturePath(...args) { + return path.join(fixtureDir, "config-hierarchy", ...args); + } - afterEach(() => { - sinon.verifyAndRestore(); - }); + // copy into clean area so as not to get "infected" by this project's .eslintrc files + before(function() { - after(() => { - sh.rm("-r", fixtureDir); - }); + /* + * GitHub Actions Windows and macOS runners occasionally exhibit + * extremely slow filesystem operations, during which copying fixtures + * exceeds the default test timeout, so raise it just for this hook. + * Mocha uses `this` to set timeouts on an individual hook level. + */ + this.timeout(60 * 1000); // eslint-disable-line no-invalid-this - it("should create config object when using baseConfig with extends", () => { - const customBaseConfig = { - extends: path.resolve(dirname, "../fixtures/config-extends/array/.eslintrc") - }; - const factory = new CascadingConfigArrayFactory({ - cwd: fixtureDir, - baseConfig: customBaseConfig, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + fixtureDir = `${systemTempDir}/eslint/fixtures`; + sh.mkdir("-p", fixtureDir); + sh.cp("-r", "./tests/fixtures/config-hierarchy", fixtureDir); + sh.cp("-r", "./tests/fixtures/rules", fixtureDir); }); - const config = getConfig(factory); - assert.deepStrictEqual(config.env, { - browser: false, - es6: true, - node: true + afterEach(() => { + sinon.verifyAndRestore(); }); - assert.deepStrictEqual(config.rules, { - "no-empty": [1], - "comma-dangle": [2], - "no-console": [2] + + after(() => { + sh.rm("-r", fixtureDir); }); - }); - // TODO: Tests should not rely on project files!!! - it("should return the project config when called in current working directory", () => { - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath + it("should create config object when using baseConfig with extends", () => { + const customBaseConfig = { + extends: path.resolve(dirname, "../fixtures/config-extends/array/.eslintrc") + }; + const factory = new CascadingConfigArrayFactory({ + cwd: fixtureDir, + baseConfig: customBaseConfig, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const config = getConfig(factory); + + assert.deepStrictEqual(config.env, { + browser: false, + es6: true, + node: true + }); + assert.deepStrictEqual(config.rules, { + "no-empty": [1], + "comma-dangle": [2], + "no-console": [2] + }); }); - const actual = getConfig(factory); - assert.strictEqual(actual.rules.strict[1], "global"); - }); + // TODO: Tests should not rely on project files!!! + it("should return the project config when called in current working directory", () => { + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); + const actual = getConfig(factory); - it("should not retain configs from previous directories when called multiple times", () => { - const firstpath = path.resolve(dirname, "../fixtures/configurations/single-quotes/subdir/.eslintrc"); - const secondpath = path.resolve(dirname, "../fixtures/configurations/single-quotes/.eslintrc"); - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath + assert.strictEqual(actual.rules.strict[1], "global"); }); - let config; - config = getConfig(factory, firstpath); - assert.deepStrictEqual(config.rules["no-new"], [0]); - config = getConfig(factory, secondpath); - assert.deepStrictEqual(config.rules["no-new"], [1]); - }); + it("should not retain configs from previous directories when called multiple times", () => { + const firstpath = path.resolve(dirname, "../fixtures/configurations/single-quotes/subdir/.eslintrc"); + const secondpath = path.resolve(dirname, "../fixtures/configurations/single-quotes/.eslintrc"); + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); + let config; - it("should throw error when a configuration file doesn't exist", () => { - const configPath = path.resolve(dirname, "../fixtures/configurations/.eslintrc"); - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath + config = getConfig(factory, firstpath); + assert.deepStrictEqual(config.rules["no-new"], [0]); + config = getConfig(factory, secondpath); + assert.deepStrictEqual(config.rules["no-new"], [1]); }); - sinon.stub(fs, "readFileSync").throws(new Error()); + it("should throw error when a configuration file doesn't exist", () => { + const configPath = path.resolve(dirname, "../fixtures/configurations/.eslintrc"); + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); - assert.throws(() => { - getConfig(factory, configPath); - }, "Cannot read config file"); + sinon.stub(fs, "readFileSync").throws(new Error()); - }); + assert.throws(() => { + getConfig(factory, configPath); + }, "Cannot read config file"); - it("should throw error when a configuration file is not require-able", () => { - const configPath = ".eslintrc"; - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath }); - sinon.stub(fs, "readFileSync").throws(new Error()); + it("should throw error when a configuration file is not require-able", () => { + const configPath = ".eslintrc"; + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); - assert.throws(() => { - getConfig(factory, configPath); - }, "Cannot read config file"); + sinon.stub(fs, "readFileSync").throws(new Error()); - }); + assert.throws(() => { + getConfig(factory, configPath); + }, "Cannot read config file"); - it("should cache config when the same directory is passed twice", () => { - const configPath = path.resolve(dirname, "../fixtures/configurations/single-quotes/.eslintrc"); - const configArrayFactory = new ConfigArrayFactory(); - const factory = new CascadingConfigArrayFactory({ - configArrayFactory, - eslintAllPath, - eslintRecommendedPath }); - sinon.spy(configArrayFactory, "loadInDirectory"); + it("should cache config when the same directory is passed twice", () => { + const configPath = path.resolve(dirname, "../fixtures/configurations/single-quotes/.eslintrc"); + const configArrayFactory = new ConfigArrayFactory(); + const factory = new CascadingConfigArrayFactory({ + configArrayFactory, + eslintAllPath, + eslintRecommendedPath + }); - // If cached this should be called only once - getConfig(factory, configPath); - const callcount = configArrayFactory.loadInDirectory.callcount; + sinon.spy(configArrayFactory, "loadInDirectory"); - getConfig(factory, configPath); + // If cached this should be called only once + getConfig(factory, configPath); + const callcount = configArrayFactory.loadInDirectory.callcount; - assert.strictEqual(configArrayFactory.loadInDirectory.callcount, callcount); - }); + getConfig(factory, configPath); - // make sure JS-style comments don't throw an error - it("should load the config file when there are JS-style comments in the text", () => { - const specificConfigPath = path.resolve(dirname, "../fixtures/configurations/comments.json"); - const factory = new CascadingConfigArrayFactory({ - specificConfigPath, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + assert.strictEqual(configArrayFactory.loadInDirectory.callcount, callcount); }); - const config = getConfig(factory); - const { semi, strict } = config.rules; - assert.deepStrictEqual(semi, [1]); - assert.deepStrictEqual(strict, [0]); - }); + // make sure JS-style comments don't throw an error + it("should load the config file when there are JS-style comments in the text", () => { + const specificConfigPath = path.resolve(dirname, "../fixtures/configurations/comments.json"); + const factory = new CascadingConfigArrayFactory({ + specificConfigPath, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const config = getConfig(factory); + const { semi, strict } = config.rules; - // make sure YAML files work correctly - it("should load the config file when a YAML file is used", () => { - const specificConfigPath = path.resolve(dirname, "../fixtures/configurations/env-browser.yaml"); - const factory = new CascadingConfigArrayFactory({ - specificConfigPath, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + assert.deepStrictEqual(semi, [1]); + assert.deepStrictEqual(strict, [0]); }); - const config = getConfig(factory); - const { "no-alert": noAlert, "no-undef": noUndef } = config.rules; - assert.deepStrictEqual(noAlert, [0]); - assert.deepStrictEqual(noUndef, [2]); - }); + // make sure YAML files work correctly + it("should load the config file when a YAML file is used", () => { + const specificConfigPath = path.resolve(dirname, "../fixtures/configurations/env-browser.yaml"); + const factory = new CascadingConfigArrayFactory({ + specificConfigPath, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const config = getConfig(factory); + const { "no-alert": noAlert, "no-undef": noUndef } = config.rules; - it("should contain the correct value for parser when a custom parser is specified", () => { - const configPath = path.resolve(dirname, "../fixtures/configurations/parser/.eslintrc.json"); - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath + assert.deepStrictEqual(noAlert, [0]); + assert.deepStrictEqual(noUndef, [2]); }); - const config = getConfig(factory, configPath); - assert.strictEqual(config.parser, path.resolve(path.dirname(configPath), "./custom.cjs")); - }); + it("should contain the correct value for parser when a custom parser is specified", () => { + const configPath = path.resolve(dirname, "../fixtures/configurations/parser/.eslintrc.json"); + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); + const config = getConfig(factory, configPath); - /* - * Configuration hierarchy --------------------------------------------- - * https://github.com/eslint/eslint/issues/3915 - */ - it("should correctly merge environment settings", () => { - const factory = new CascadingConfigArrayFactory({ - useEslintrc: true, - eslintAllPath, - eslintRecommendedPath - }); - const file = getFixturePath("envs", "sub", "foo.js"); - const expected = { - rules: {}, - env: { - browser: true, - node: false - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); + assert.strictEqual(config.parser, path.resolve(path.dirname(configPath), "./custom.cjs")); + }); - assertConfigsEqual(actual, expected); - }); + /* + * Configuration hierarchy --------------------------------------------- + * https://github.com/eslint/eslint/issues/3915 + */ + it("should correctly merge environment settings", () => { + const factory = new CascadingConfigArrayFactory({ + useEslintrc: true, + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("envs", "sub", "foo.js"); + const expected = { + rules: {}, + env: { + browser: true, + node: false + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - // Default configuration - blank - it("should return a blank config when using no .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath - }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); - const expected = { - rules: {}, - globals: {}, - env: {}, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); + assertConfigsEqual(actual, expected); + }); - assertConfigsEqual(actual, expected); - }); + // Default configuration - blank + it("should return a blank config when using no .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + rules: {}, + globals: {}, + env: {}, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - it("should return a blank config when baseConfig is set to false and no .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - baseConfig: false, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath - }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); - const expected = { - rules: {}, - globals: {}, - env: {}, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); + assertConfigsEqual(actual, expected); + }); - assertConfigsEqual(actual, expected); - }); + it("should return a blank config when baseConfig is set to false and no .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + baseConfig: false, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + rules: {}, + globals: {}, + env: {}, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - // No default configuration - it("should return an empty config when not using .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + assertConfigsEqual(actual, expected); }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); - const actual = getConfig(factory, file); - assertConfigsEqual(actual, { ignorePatterns: cwdIgnorePatterns }); - }); + // No default configuration + it("should return an empty config when not using .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, { ignorePatterns: cwdIgnorePatterns }); + }); - it("should return a modified config when baseConfig is set to an object and no .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - baseConfig: { + it("should return a modified config when baseConfig is set to an object and no .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + baseConfig: { + env: { + node: true + }, + rules: { + quotes: [2, "single"] + } + }, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { env: { node: true }, rules: { quotes: [2, "single"] - } - }, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath - }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - rules: { - quotes: [2, "single"] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); - }); + assertConfigsEqual(actual, expected); + }); - it("should return a modified config without plugin rules enabled when baseConfig is set to an object with plugin and no .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - baseConfig: { + it("should return a modified config without plugin rules enabled when baseConfig is set to an object with plugin and no .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + baseConfig: { + env: { + node: true + }, + rules: { + quotes: [2, "single"] + }, + plugins: ["example-with-rules-config"] + }, + cwd: getFixturePath("plugins"), + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"); + const expected = { env: { node: true }, + plugins: ["example-with-rules-config"], rules: { quotes: [2, "single"] - }, - plugins: ["example-with-rules-config"] - }, - cwd: getFixturePath("plugins"), - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath - }); - const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - plugins: ["example-with-rules-config"], - rules: { - quotes: [2, "single"] - } - }; - const actual = getConfig(factory, file); - - assertConfigsEqual(actual, expected); - }); - - // Project configuration - second level .eslintrc - it("should merge configs when local .eslintrc overrides parent .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath - }); - const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - rules: { - "no-console": [1], - quotes: [2, "single"] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); - - assertConfigsEqual(actual, expected); - }); + } + }; + const actual = getConfig(factory, file); - // Project configuration - third level .eslintrc - it("should merge configs when local .eslintrc overrides parent and grandparent .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath + assertConfigsEqual(actual, expected); }); - const file = getFixturePath("broken", "subbroken", "subsubbroken", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - rules: { - "no-console": [0], - quotes: [1, "double"] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); - }); + // Project configuration - second level .eslintrc + it("should merge configs when local .eslintrc overrides parent .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + "no-console": [1], + quotes: [2, "single"] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - // Project configuration - root set in second level .eslintrc - it("should not return or traverse configurations in parents of config with root:true", () => { - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath + assertConfigsEqual(actual, expected); }); - const file = getFixturePath("root-true", "parent", "root", "wrong-semi.js"); - const expected = { - rules: { - semi: [2, "never"] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); - }); + // Project configuration - third level .eslintrc + it("should merge configs when local .eslintrc overrides parent and grandparent .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "subbroken", "subsubbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + "no-console": [0], + quotes: [1, "double"] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - // Project configuration - root set in second level .eslintrc - it("should return project config when called with a relative path from a subdir", () => { - const factory = new CascadingConfigArrayFactory({ - cwd: getFixturePath("root-true", "parent", "root", "subdir"), - eslintRecommendedPath, - eslintAllPath + assertConfigsEqual(actual, expected); }); - const dir = "."; - const expected = { - rules: { - semi: [2, "never"] - } - }; - const actual = getConfig(factory, dir); - assertConfigsEqual(actual, expected); - }); + // Project configuration - root set in second level .eslintrc + it("should not return or traverse configurations in parents of config with root:true", () => { + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("root-true", "parent", "root", "wrong-semi.js"); + const expected = { + rules: { + semi: [2, "never"] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - // Command line configuration - --config with first level .eslintrc - it("should merge command line config when config file adds to local .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: getFixturePath("broken", "add-conf.yaml"), - eslintAllPath, - eslintRecommendedPath + assertConfigsEqual(actual, expected); }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - rules: { - quotes: [2, "double"], - semi: [1, "never"] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); - }); + // Project configuration - root set in second level .eslintrc + it("should return project config when called with a relative path from a subdir", () => { + const factory = new CascadingConfigArrayFactory({ + cwd: getFixturePath("root-true", "parent", "root", "subdir"), + eslintAllPath, + eslintRecommendedPath + }); + const dir = "."; + const expected = { + rules: { + semi: [2, "never"] + } + }; + const actual = getConfig(factory, dir); - // Command line configuration - --config with first level .eslintrc - it("should merge command line config when config file overrides local .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: getFixturePath("broken", "override-conf.yaml"), - eslintAllPath, - eslintRecommendedPath + assertConfigsEqual(actual, expected); }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - rules: { - quotes: [0, "double"] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); - }); + // Command line configuration - --config with first level .eslintrc + it("should merge command line config when config file adds to local .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: getFixturePath("broken", "add-conf.yaml"), + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [2, "double"], + semi: [1, "never"] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - // Command line configuration - --config with second level .eslintrc - it("should merge command line config when config file adds to local and parent .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: getFixturePath("broken", "add-conf.yaml"), - eslintAllPath, - eslintRecommendedPath + assertConfigsEqual(actual, expected); }); - const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - rules: { - quotes: [2, "single"], - "no-console": [1], - semi: [1, "never"] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); - }); + // Command line configuration - --config with first level .eslintrc + it("should merge command line config when config file overrides local .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: getFixturePath("broken", "override-conf.yaml"), + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [0, "double"] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - // Command line configuration - --config with second level .eslintrc - it("should merge command line config when config file overrides local and parent .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: getFixturePath("broken", "override-conf.yaml"), - eslintAllPath, - eslintRecommendedPath + assertConfigsEqual(actual, expected); }); - const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - rules: { - quotes: [0, "single"], - "no-console": [1] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); - }); + // Command line configuration - --config with second level .eslintrc + it("should merge command line config when config file adds to local and parent .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: getFixturePath("broken", "add-conf.yaml"), + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [2, "single"], + "no-console": [1], + semi: [1, "never"] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); - // Command line configuration - --rule with --config and first level .eslintrc - it("should merge command line config and rule when rule and config file overrides local .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - cliConfig: { + // Command line configuration - --config with second level .eslintrc + it("should merge command line config when config file overrides local and parent .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: getFixturePath("broken", "override-conf.yaml"), + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [0, "single"], + "no-console": [1] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + // Command line configuration - --rule with --config and first level .eslintrc + it("should merge command line config and rule when rule and config file overrides local .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + cliConfig: { + rules: { + quotes: [1, "double"] + } + }, + specificConfigPath: getFixturePath("broken", "override-conf.yaml"), + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, rules: { quotes: [1, "double"] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + // Command line configuration - --plugin + it("should merge command line plugin with local .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + cliConfig: { + plugins: ["another-plugin"] + }, + cwd: getFixturePath("plugins"), + resolvePluginsRelativeTo: getFixturePath("plugins"), + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + plugins: [ + "example", + "another-plugin" + ], + rules: { + quotes: [2, "double"] } - }, - specificConfigPath: getFixturePath("broken", "override-conf.yaml"), - eslintAllPath, - eslintRecommendedPath - }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - rules: { - quotes: [1, "double"] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); + }; + const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); - }); + assertConfigsEqual(actual, expected); + }); - // Command line configuration - --plugin - it("should merge command line plugin with local .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - cliConfig: { - plugins: ["another-plugin"] - }, - cwd: getFixturePath("plugins"), - resolvePluginsRelativeTo: getFixturePath("plugins"), - eslintAllPath, - eslintRecommendedPath - }); - const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - plugins: [ - "example", - "another-plugin" - ], - rules: { - quotes: [2, "double"] + + it("should merge multiple different config file formats", () => { + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("fileexts/subdir/subsubdir/foo.js"); + const expected = { + env: { + browser: true + }, + rules: { + semi: [2, "always"], + eqeqeq: [2] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + + it("should load user config globals", () => { + const configPath = path.resolve(dirname, "../fixtures/globals/conf.yaml"); + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: configPath, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const expected = { + globals: { + foo: true + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, configPath); + + assertConfigsEqual(actual, expected); + }); + + it("should not load disabled environments", () => { + const configPath = path.resolve(dirname, "../fixtures/environments/disable.yaml"); + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: configPath, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const config = getConfig(factory, configPath); + + assert.isUndefined(config.globals.window); + }); + + it("should gracefully handle empty files", () => { + const configPath = path.resolve(dirname, "../fixtures/configurations/env-node.json"); + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: configPath, + eslintAllPath, + eslintRecommendedPath + }); + + getConfig(factory, path.resolve(dirname, "../fixtures/configurations/empty/empty.json")); + }); + + // Meaningful stack-traces + it("should include references to where an `extends` configuration was loaded from", () => { + const configPath = path.resolve(dirname, "../fixtures/config-extends/error.json"); + + assert.throws(() => { + const factory = new CascadingConfigArrayFactory({ + useEslintrc: false, + specificConfigPath: configPath, + eslintAllPath, + eslintRecommendedPath + }); + + getConfig(factory, configPath); + }, /Referenced from:.*?error\.json/u); + }); + + // Keep order with the last array element taking highest precedence + it("should make the last element in an array take the highest precedence", () => { + const configPath = path.resolve(dirname, "../fixtures/config-extends/array/.eslintrc"); + const factory = new CascadingConfigArrayFactory({ + useEslintrc: false, + specificConfigPath: configPath, + eslintAllPath, + eslintRecommendedPath + }); + const expected = { + rules: { "no-empty": [1], "comma-dangle": [2], "no-console": [2] }, + env: { browser: false, node: true, es6: true }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, configPath); + + assertConfigsEqual(actual, expected); + }); + + describe("with env in a child configuration file", () => { + it("should not overwrite parserOptions of the parent with env of the child", () => { + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); + const targetPath = getFixturePath("overwrite-ecmaFeatures", "child", "foo.js"); + const expected = { + rules: {}, + env: { commonjs: true }, + parserOptions: { ecmaFeatures: { globalReturn: false } }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + }); + + describe("personal config file within home directory", () => { + + const root = path.join(systemTempDir, "eslint/cli-engine/cascading-config-array-factory/personal-config"); + + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root, + files: { + ...DIRECTORY_CONFIG_HIERARCHY + } + }); + + before(prepare); + after(cleanup); + + /** + * Returns the path inside of the fixture directory. + * @param {...string} args file path segments. + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFakeFixturePath(...args) { + return path.join(getPath(), "eslint", "fixtures", "config-hierarchy", ...args); } - }; - const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); + it("should load the personal config if no local config was found", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "home-folder"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + eslintAllPath, + eslintRecommendedPath + }); + + mockOsHomedir(homePath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + "home-folder-rule": [2] + } + }; + + assertConfigsEqual(actual, expected); + }); + + it("should ignore the personal config if a local config was found", () => { + const projectPath = getFakeFixturePath("personal-config", "home-folder", "project"); + const homePath = getFakeFixturePath("personal-config", "home-folder"); + const filePath = getFakeFixturePath("personal-config", "home-folder", "project", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + eslintAllPath, + eslintRecommendedPath + }); + + mockOsHomedir(homePath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + "project-level-rule": [2] + } + }; + + assertConfigsEqual(actual, expected); + }); + + it("should ignore the personal config if config is passed through cli", () => { + const configPath = getFakeFixturePath("quotes-error.json"); + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "home-folder"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + specificConfigPath: configPath, + eslintAllPath, + eslintRecommendedPath + }); + + mockOsHomedir(homePath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + quotes: [2, "double"] + } + }; + + assertConfigsEqual(actual, expected); + }); + + it("should still load the project config if the current working directory is the same as the home folder", () => { + const projectPath = getFakeFixturePath("personal-config", "project-with-config"); + const filePath = getFakeFixturePath("personal-config", "project-with-config", "subfolder", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + eslintAllPath, + eslintRecommendedPath + }); + + mockOsHomedir(projectPath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + "project-level-rule": [2], + "subfolder-level-rule": [2] + } + }; + + assertConfigsEqual(actual, expected); + }); + }); + + describe("when no local or personal config is found", () => { + + const root = path.join(systemTempDir, "eslint/cli-engine/cascading-config-array-factory/personal-config"); + + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root, + files: { + ...DIRECTORY_CONFIG_HIERARCHY + } + }); + + before(prepare); + after(cleanup); + + /** + * Returns the path inside of the fixture directory. + * @param {...string} args file path segments. + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFakeFixturePath(...args) { + return path.join(getPath(), "eslint", "fixtures", "config-hierarchy", ...args); + } + + it("should throw an error if no local config and no personal config was found", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + eslintAllPath, + eslintRecommendedPath + }); + + mockOsHomedir(homePath); + + assert.throws(() => { + getConfig(factory, filePath); + }, "No ESLint configuration found"); + }); + + it("should throw an error if no local config was found and ~/package.json contains no eslintConfig section", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "home-folder-with-packagejson"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + eslintAllPath, + eslintRecommendedPath + }); + + mockOsHomedir(homePath); + + assert.throws(() => { + getConfig(factory, filePath); + }, "No ESLint configuration found"); + }); + + it("should not throw an error if no local config and no personal config was found but useEslintrc is false", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + + mockOsHomedir(homePath); + + getConfig(factory, filePath); + }); + + it("should not throw an error if no local config and no personal config was found but rules are specified", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cliConfig: { + rules: { quotes: [2, "single"] } + }, + cwd: projectPath, + eslintAllPath, + eslintRecommendedPath + }); + + mockOsHomedir(homePath); + + getConfig(factory, filePath); + }); + + it("should not throw an error if no local config and no personal config was found but baseConfig is specified", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + baseConfig: {}, + cwd: projectPath, + eslintAllPath, + eslintRecommendedPath + }); + + mockOsHomedir(homePath); + + getConfig(factory, filePath); + }); + }); + + describe("with overrides", () => { + + const root = path.join(systemTempDir, "eslint/cli-engine/cascading-config-array-factory/personal-config"); + + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root, + files: { + ...DIRECTORY_CONFIG_HIERARCHY + } + }); + + before(prepare); + after(cleanup); + + /** + * Returns the path inside of the fixture directory. + * @param {...string} pathSegments One or more path segments, in order of depth, shallowest first + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFakeFixturePath(...pathSegments) { + return path.join(getPath(), "eslint", "fixtures", "config-hierarchy", ...pathSegments); + } + + it("should merge override config when the pattern matches the file name", () => { + const factory = new CascadingConfigArrayFactory({ + cwd: getPath(), + eslintAllPath, + eslintRecommendedPath + }); + const targetPath = getFakeFixturePath("overrides", "foo.js"); + const expected = { + rules: { + quotes: [2, "single"], + "no-else-return": [0], + "no-unused-vars": [1], + semi: [1, "never"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should merge override config when the pattern matches the file path relative to the config file", () => { + const factory = new CascadingConfigArrayFactory({ + cwd: getPath(), + eslintAllPath, + eslintRecommendedPath + }); + const targetPath = getFakeFixturePath("overrides", "child", "child-one.js"); + const expected = { + rules: { + curly: ["error", "multi", "consistent"], + "no-else-return": [0], + "no-unused-vars": [1], + quotes: [2, "double"], + semi: [1, "never"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should not merge override config when the pattern matches the absolute file path", () => { + const resolvedPath = path.resolve(dirname, "../fixtures/config-hierarchy/overrides/bar.cjs"); + + assert.throws(() => new CascadingConfigArrayFactory({ + cwd: getPath(), + baseConfig: { + overrides: [{ + files: resolvedPath, + rules: { + quotes: [1, "double"] + } + }] + }, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }), /Invalid override pattern/u); + }); + + it("should not merge override config when the pattern traverses up the directory tree", () => { + const parentPath = "overrides/../**/*.js"; + + assert.throws(() => new CascadingConfigArrayFactory({ + baseConfig: { + overrides: [{ + files: parentPath, + rules: { + quotes: [1, "single"] + } + }] + }, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }), /Invalid override pattern/u); + }); + + it("should merge all local configs (override and non-override) before non-local configs", () => { + const factory = new CascadingConfigArrayFactory({ + cwd: getPath(), + eslintAllPath, + eslintRecommendedPath + }); + const targetPath = getFakeFixturePath("overrides", "two", "child-two.js"); + const expected = { + rules: { + "no-console": [0], + "no-else-return": [0], + "no-unused-vars": [2], + quotes: [2, "double"], + semi: [2, "never"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides in parent .eslintrc over non-override rules in child .eslintrc", () => { + const targetPath = getFakeFixturePath("overrides", "three", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [ + { + files: "three/**/*.js", + rules: { + "semi-style": [2, "last"] + } + } + ] + }, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const expected = { + rules: { + "semi-style": [2, "last"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides if all glob patterns match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: ["one/**/*", "*.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const expected = { + rules: { + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides even if some glob patterns do not match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: ["one/**/*", "*two.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const expected = { + rules: { + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should not apply overrides if any excluded glob patterns match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: "one/**/*", + excludedFiles: ["two/**/*", "*one.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const expected = { + rules: {} + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides if all excluded glob patterns fail to match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: "one/**/*", + excludedFiles: ["two/**/*", "*two.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const expected = { + rules: { + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should cascade", () => { + const targetPath = getFakeFixturePath("overrides", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [ + { + files: "foo.js", + rules: { + semi: [2, "never"], + quotes: [2, "single"] + } + }, + { + files: "foo.js", + rules: { + semi: [2, "never"], + quotes: [2, "double"] + } + } + ] + }, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const expected = { + rules: { + semi: [2, "never"], + quotes: [2, "double"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + }); + + describe("deprecation warnings", () => { + const cwd = path.resolve(dirname, "../fixtures/config-file/"); + let warning = null; + + /** + * Store a reported warning object if that code starts with `ESLINT_`. + * @param {{code:string, message:string}} w The warning object to store. + * @returns {void} + */ + function onWarning(w) { + if (w.code.startsWith("ESLINT_")) { + warning = w; + } + } + + /** @type {CascadingConfigArrayFactory} */ + let factory; + + beforeEach(() => { + factory = new CascadingConfigArrayFactory({ + cwd, + eslintAllPath, + eslintRecommendedPath + }); + warning = null; + process.on("warning", onWarning); + }); + afterEach(() => { + process.removeListener("warning", onWarning); + }); + + it("should emit a deprecation warning if 'ecmaFeatures' is given.", async () => { + getConfig(factory, "ecma-features/test.js"); + + // Wait for "warning" event. + await nextTick(); + + assert.notStrictEqual(warning, null); + assert.strictEqual( + warning.message, + `The 'ecmaFeatures' config file property is deprecated and has no effect. (found in "ecma-features${path.sep}.eslintrc.yml")` + ); + }); + }); }); + describe("with eslint built-in config callbacks", () => { + let fixtureDir; - it("should merge multiple different config file formats", () => { - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath + /** + * Returns the path inside of the fixture directory. + * @param {...string} args file path segments. + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFixturePath(...args) { + return path.join(fixtureDir, "config-hierarchy", ...args); + } + + // copy into clean area so as not to get "infected" by this project's .eslintrc files + before(function() { + + /* + * GitHub Actions Windows and macOS runners occasionally exhibit + * extremely slow filesystem operations, during which copying fixtures + * exceeds the default test timeout, so raise it just for this hook. + * Mocha uses `this` to set timeouts on an individual hook level. + */ + this.timeout(60 * 1000); // eslint-disable-line no-invalid-this + + fixtureDir = `${systemTempDir}/eslint/fixtures`; + sh.mkdir("-p", fixtureDir); + sh.cp("-r", "./tests/fixtures/config-hierarchy", fixtureDir); + sh.cp("-r", "./tests/fixtures/rules", fixtureDir); }); - const file = getFixturePath("fileexts/subdir/subsubdir/foo.js"); - const expected = { - env: { - browser: true - }, - rules: { - semi: [2, "always"], - eqeqeq: [2] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); - }); + afterEach(() => { + sinon.verifyAndRestore(); + }); + + after(() => { + sh.rm("-r", fixtureDir); + }); + + it("should create config object when using baseConfig with extends", () => { + const customBaseConfig = { + extends: path.resolve(dirname, "../fixtures/config-extends/array/.eslintrc") + }; + const factory = new CascadingConfigArrayFactory({ + cwd: fixtureDir, + baseConfig: customBaseConfig, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + const config = getConfig(factory); + + assert.deepStrictEqual(config.env, { + browser: false, + es6: true, + node: true + }); + assert.deepStrictEqual(config.rules, { + "no-empty": [1], + "comma-dangle": [2], + "no-console": [2] + }); + }); + + // TODO: Tests should not rely on project files!!! + it("should return the project config when called in current working directory", () => { + const factory = new CascadingConfigArrayFactory({ + getEslintAllConfig, + getEslintRecommendedConfig + }); + const actual = getConfig(factory); + + assert.strictEqual(actual.rules.strict[1], "global"); + }); + + it("should not retain configs from previous directories when called multiple times", () => { + const firstpath = path.resolve(dirname, "../fixtures/configurations/single-quotes/subdir/.eslintrc"); + const secondpath = path.resolve(dirname, "../fixtures/configurations/single-quotes/.eslintrc"); + const factory = new CascadingConfigArrayFactory({ + getEslintAllConfig, + getEslintRecommendedConfig + }); + let config; + + config = getConfig(factory, firstpath); + assert.deepStrictEqual(config.rules["no-new"], [0]); + config = getConfig(factory, secondpath); + assert.deepStrictEqual(config.rules["no-new"], [1]); + }); + + it("should throw error when a configuration file doesn't exist", () => { + const configPath = path.resolve(dirname, "../fixtures/configurations/.eslintrc"); + const factory = new CascadingConfigArrayFactory({ + getEslintAllConfig, + getEslintRecommendedConfig + }); + + sinon.stub(fs, "readFileSync").throws(new Error()); + + assert.throws(() => { + getConfig(factory, configPath); + }, "Cannot read config file"); + + }); + + it("should throw error when a configuration file is not require-able", () => { + const configPath = ".eslintrc"; + const factory = new CascadingConfigArrayFactory({ + getEslintAllConfig, + getEslintRecommendedConfig + }); + + sinon.stub(fs, "readFileSync").throws(new Error()); + assert.throws(() => { + getConfig(factory, configPath); + }, "Cannot read config file"); - it("should load user config globals", () => { - const configPath = path.resolve(dirname, "../fixtures/globals/conf.yaml"); - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: configPath, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath }); - const expected = { - globals: { - foo: true - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, configPath); - assertConfigsEqual(actual, expected); - }); + it("should cache config when the same directory is passed twice", () => { + const configPath = path.resolve(dirname, "../fixtures/configurations/single-quotes/.eslintrc"); + const configArrayFactory = new ConfigArrayFactory(); + const factory = new CascadingConfigArrayFactory({ + configArrayFactory, + getEslintAllConfig, + getEslintRecommendedConfig + }); - it("should not load disabled environments", () => { - const configPath = path.resolve(dirname, "../fixtures/environments/disable.yaml"); - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: configPath, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath - }); - const config = getConfig(factory, configPath); + sinon.spy(configArrayFactory, "loadInDirectory"); - assert.isUndefined(config.globals.window); - }); + // If cached this should be called only once + getConfig(factory, configPath); + const callcount = configArrayFactory.loadInDirectory.callcount; + + getConfig(factory, configPath); - it("should gracefully handle empty files", () => { - const configPath = path.resolve(dirname, "../fixtures/configurations/env-node.json"); - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: configPath, - eslintAllPath, - eslintRecommendedPath + assert.strictEqual(configArrayFactory.loadInDirectory.callcount, callcount); }); - getConfig(factory, path.resolve(dirname, "../fixtures/configurations/empty/empty.json")); - }); + // make sure JS-style comments don't throw an error + it("should load the config file when there are JS-style comments in the text", () => { + const specificConfigPath = path.resolve(dirname, "../fixtures/configurations/comments.json"); + const factory = new CascadingConfigArrayFactory({ + specificConfigPath, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + const config = getConfig(factory); + const { semi, strict } = config.rules; - // Meaningful stack-traces - it("should include references to where an `extends` configuration was loaded from", () => { - const configPath = path.resolve(dirname, "../fixtures/config-extends/error.json"); + assert.deepStrictEqual(semi, [1]); + assert.deepStrictEqual(strict, [0]); + }); - assert.throws(() => { + // make sure YAML files work correctly + it("should load the config file when a YAML file is used", () => { + const specificConfigPath = path.resolve(dirname, "../fixtures/configurations/env-browser.yaml"); const factory = new CascadingConfigArrayFactory({ + specificConfigPath, useEslintrc: false, - specificConfigPath: configPath, - eslintAllPath, - eslintRecommendedPath + getEslintAllConfig, + getEslintRecommendedConfig }); + const config = getConfig(factory); + const { "no-alert": noAlert, "no-undef": noUndef } = config.rules; - getConfig(factory, configPath); - }, /Referenced from:.*?error\.json/u); - }); + assert.deepStrictEqual(noAlert, [0]); + assert.deepStrictEqual(noUndef, [2]); + }); - // Keep order with the last array element taking highest precedence - it("should make the last element in an array take the highest precedence", () => { - const configPath = path.resolve(dirname, "../fixtures/config-extends/array/.eslintrc"); - const factory = new CascadingConfigArrayFactory({ - useEslintrc: false, - specificConfigPath: configPath, - eslintAllPath, - eslintRecommendedPath - }); - const expected = { - rules: { "no-empty": [1], "comma-dangle": [2], "no-console": [2] }, - env: { browser: false, node: true, es6: true }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, configPath); + it("should contain the correct value for parser when a custom parser is specified", () => { + const configPath = path.resolve(dirname, "../fixtures/configurations/parser/.eslintrc.json"); + const factory = new CascadingConfigArrayFactory({ + getEslintAllConfig, + getEslintRecommendedConfig + }); + const config = getConfig(factory, configPath); - assertConfigsEqual(actual, expected); - }); + assert.strictEqual(config.parser, path.resolve(path.dirname(configPath), "./custom.cjs")); + }); - describe("with env in a child configuration file", () => { - it("should not overwrite parserOptions of the parent with env of the child", () => { + /* + * Configuration hierarchy --------------------------------------------- + * https://github.com/eslint/eslint/issues/3915 + */ + it("should correctly merge environment settings", () => { const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath + useEslintrc: true, + getEslintAllConfig, + getEslintRecommendedConfig }); - const targetPath = getFixturePath("overwrite-ecmaFeatures", "child", "foo.js"); + const file = getFixturePath("envs", "sub", "foo.js"); const expected = { rules: {}, - env: { commonjs: true }, - parserOptions: { ecmaFeatures: { globalReturn: false } }, + env: { + browser: true, + node: false + }, ignorePatterns: cwdIgnorePatterns }; - const actual = getConfig(factory, targetPath); + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - }); - - describe("personal config file within home directory", () => { - - const root = path.join(systemTempDir, "eslint/cli-engine/cascading-config-array-factory/personal-config"); - - const { prepare, cleanup, getPath } = createCustomTeardown({ - cwd: root, - files: { - ...DIRECTORY_CONFIG_HIERARCHY - } - }); - - before(prepare); - after(cleanup); - - /** - * Returns the path inside of the fixture directory. - * @param {...string} args file path segments. - * @returns {string} The path inside the fixture directory. - * @private - */ - function getFakeFixturePath(...args) { - return path.join(getPath(), "eslint", "fixtures", "config-hierarchy", ...args); - } - it("should load the personal config if no local config was found", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "home-folder"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + // Default configuration - blank + it("should return a blank config when using no .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: projectPath, - eslintAllPath, - eslintRecommendedPath + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig }); - - mockOsHomedir(homePath); - - const actual = getConfig(factory, filePath); + const file = getFixturePath("broken", "console-wrong-quotes.js"); const expected = { - rules: { - "home-folder-rule": [2] - } + rules: {}, + globals: {}, + env: {}, + ignorePatterns: cwdIgnorePatterns }; + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - it("should ignore the personal config if a local config was found", () => { - const projectPath = getFakeFixturePath("personal-config", "home-folder", "project"); - const homePath = getFakeFixturePath("personal-config", "home-folder"); - const filePath = getFakeFixturePath("personal-config", "home-folder", "project", "foo.js"); + it("should return a blank config when baseConfig is set to false and no .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: projectPath, - eslintAllPath, - eslintRecommendedPath + baseConfig: false, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig }); - - mockOsHomedir(homePath); - - const actual = getConfig(factory, filePath); + const file = getFixturePath("broken", "console-wrong-quotes.js"); const expected = { - rules: { - "project-level-rule": [2] - } + rules: {}, + globals: {}, + env: {}, + ignorePatterns: cwdIgnorePatterns }; + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - it("should ignore the personal config if config is passed through cli", () => { - const configPath = getFakeFixturePath("quotes-error.json"); - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "home-folder"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + // No default configuration + it("should return an empty config when not using .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: projectPath, - specificConfigPath: configPath, - eslintAllPath, - eslintRecommendedPath + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const actual = getConfig(factory, file); - mockOsHomedir(homePath); + assertConfigsEqual(actual, { ignorePatterns: cwdIgnorePatterns }); + }); - const actual = getConfig(factory, filePath); + it("should return a modified config when baseConfig is set to an object and no .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + baseConfig: { + env: { + node: true + }, + rules: { + quotes: [2, "single"] + } + }, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); const expected = { + env: { + node: true + }, rules: { - quotes: [2, "double"] - } + quotes: [2, "single"] + }, + ignorePatterns: cwdIgnorePatterns }; + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - it("should still load the project config if the current working directory is the same as the home folder", () => { - const projectPath = getFakeFixturePath("personal-config", "project-with-config"); - const filePath = getFakeFixturePath("personal-config", "project-with-config", "subfolder", "foo.js"); + it("should return a modified config without plugin rules enabled when baseConfig is set to an object with plugin and no .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: projectPath, - eslintAllPath, - eslintRecommendedPath + baseConfig: { + env: { + node: true + }, + rules: { + quotes: [2, "single"] + }, + plugins: ["example-with-rules-config"] + }, + cwd: getFixturePath("plugins"), + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig }); - - mockOsHomedir(projectPath); - - const actual = getConfig(factory, filePath); + const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"); const expected = { + env: { + node: true + }, + plugins: ["example-with-rules-config"], rules: { - "project-level-rule": [2], - "subfolder-level-rule": [2] + quotes: [2, "single"] } }; + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - }); - - describe("when no local or personal config is found", () => { - - const root = path.join(systemTempDir, "eslint/cli-engine/cascading-config-array-factory/personal-config"); - - const { prepare, cleanup, getPath } = createCustomTeardown({ - cwd: root, - files: { - ...DIRECTORY_CONFIG_HIERARCHY - } - }); - - before(prepare); - after(cleanup); - - /** - * Returns the path inside of the fixture directory. - * @param {...string} args file path segments. - * @returns {string} The path inside the fixture directory. - * @private - */ - function getFakeFixturePath(...args) { - return path.join(getPath(), "eslint", "fixtures", "config-hierarchy", ...args); - } - it("should throw an error if no local config and no personal config was found", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + // Project configuration - second level .eslintrc + it("should merge configs when local .eslintrc overrides parent .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: projectPath, - eslintAllPath, - eslintRecommendedPath + getEslintAllConfig, + getEslintRecommendedConfig }); + const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + "no-console": [1], + quotes: [2, "single"] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - mockOsHomedir(homePath); - - assert.throws(() => { - getConfig(factory, filePath); - }, "No ESLint configuration found"); + assertConfigsEqual(actual, expected); }); - it("should throw an error if no local config was found and ~/package.json contains no eslintConfig section", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "home-folder-with-packagejson"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + // Project configuration - third level .eslintrc + it("should merge configs when local .eslintrc overrides parent and grandparent .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: projectPath, - eslintAllPath, - eslintRecommendedPath + getEslintAllConfig, + getEslintRecommendedConfig }); + const file = getFixturePath("broken", "subbroken", "subsubbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + "no-console": [0], + quotes: [1, "double"] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - mockOsHomedir(homePath); - - assert.throws(() => { - getConfig(factory, filePath); - }, "No ESLint configuration found"); + assertConfigsEqual(actual, expected); }); - it("should not throw an error if no local config and no personal config was found but useEslintrc is false", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + // Project configuration - root set in second level .eslintrc + it("should not return or traverse configurations in parents of config with root:true", () => { const factory = new CascadingConfigArrayFactory({ - cwd: projectPath, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + getEslintAllConfig, + getEslintRecommendedConfig }); - - mockOsHomedir(homePath); - - getConfig(factory, filePath); - }); - - it("should not throw an error if no local config and no personal config was found but rules are specified", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - const factory = new CascadingConfigArrayFactory({ - cliConfig: { - rules: { quotes: [2, "single"] } + const file = getFixturePath("root-true", "parent", "root", "wrong-semi.js"); + const expected = { + rules: { + semi: [2, "never"] }, - cwd: projectPath, - eslintAllPath, - eslintRecommendedPath - }); - - mockOsHomedir(homePath); + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - getConfig(factory, filePath); + assertConfigsEqual(actual, expected); }); - it("should not throw an error if no local config and no personal config was found but baseConfig is specified", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + // Project configuration - root set in second level .eslintrc + it("should return project config when called with a relative path from a subdir", () => { const factory = new CascadingConfigArrayFactory({ - baseConfig: {}, - cwd: projectPath, - eslintAllPath, - eslintRecommendedPath + cwd: getFixturePath("root-true", "parent", "root", "subdir"), + getEslintAllConfig, + getEslintRecommendedConfig }); + const dir = "."; + const expected = { + rules: { + semi: [2, "never"] + } + }; + const actual = getConfig(factory, dir); - mockOsHomedir(homePath); - - getConfig(factory, filePath); - }); - }); - - describe("with overrides", () => { - - const root = path.join(systemTempDir, "eslint/cli-engine/cascading-config-array-factory/personal-config"); - - const { prepare, cleanup, getPath } = createCustomTeardown({ - cwd: root, - files: { - ...DIRECTORY_CONFIG_HIERARCHY - } + assertConfigsEqual(actual, expected); }); - before(prepare); - after(cleanup); - - /** - * Returns the path inside of the fixture directory. - * @param {...string} pathSegments One or more path segments, in order of depth, shallowest first - * @returns {string} The path inside the fixture directory. - * @private - */ - function getFakeFixturePath(...pathSegments) { - return path.join(getPath(), "eslint", "fixtures", "config-hierarchy", ...pathSegments); - } - - it("should merge override config when the pattern matches the file name", () => { + // Command line configuration - --config with first level .eslintrc + it("should merge command line config when config file adds to local .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: getPath(), - eslintAllPath, - eslintRecommendedPath + specificConfigPath: getFixturePath("broken", "add-conf.yaml"), + getEslintAllConfig, + getEslintRecommendedConfig }); - const targetPath = getFakeFixturePath("overrides", "foo.js"); + const file = getFixturePath("broken", "console-wrong-quotes.js"); const expected = { + env: { + node: true + }, rules: { - quotes: [2, "single"], - "no-else-return": [0], - "no-unused-vars": [1], + quotes: [2, "double"], semi: [1, "never"] - } + }, + ignorePatterns: cwdIgnorePatterns }; - const actual = getConfig(factory, targetPath); + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - it("should merge override config when the pattern matches the file path relative to the config file", () => { + // Command line configuration - --config with first level .eslintrc + it("should merge command line config when config file overrides local .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: getPath(), - eslintAllPath, - eslintRecommendedPath + specificConfigPath: getFixturePath("broken", "override-conf.yaml"), + getEslintAllConfig, + getEslintRecommendedConfig }); - const targetPath = getFakeFixturePath("overrides", "child", "child-one.js"); + const file = getFixturePath("broken", "console-wrong-quotes.js"); const expected = { + env: { + node: true + }, rules: { - curly: ["error", "multi", "consistent"], - "no-else-return": [0], - "no-unused-vars": [1], - quotes: [2, "double"], - semi: [1, "never"] - } + quotes: [0, "double"] + }, + ignorePatterns: cwdIgnorePatterns }; - const actual = getConfig(factory, targetPath); + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - it("should not merge override config when the pattern matches the absolute file path", () => { - const resolvedPath = path.resolve(dirname, "../fixtures/config-hierarchy/overrides/bar.cjs"); - - assert.throws(() => new CascadingConfigArrayFactory({ - cwd: getPath(), - baseConfig: { - overrides: [{ - files: resolvedPath, - rules: { - quotes: [1, "double"] - } - }] + // Command line configuration - --config with second level .eslintrc + it("should merge command line config when config file adds to local and parent .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: getFixturePath("broken", "add-conf.yaml"), + getEslintAllConfig, + getEslintRecommendedConfig + }); + const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true }, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath - }), /Invalid override pattern/u); - }); - - it("should not merge override config when the pattern traverses up the directory tree", () => { - const parentPath = "overrides/../**/*.js"; - - assert.throws(() => new CascadingConfigArrayFactory({ - baseConfig: { - overrides: [{ - files: parentPath, - rules: { - quotes: [1, "single"] - } - }] + rules: { + quotes: [2, "single"], + "no-console": [1], + semi: [1, "never"] }, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath - }), /Invalid override pattern/u); + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); }); - it("should merge all local configs (override and non-override) before non-local configs", () => { + // Command line configuration - --config with second level .eslintrc + it("should merge command line config when config file overrides local and parent .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: getPath(), - eslintAllPath, - eslintRecommendedPath + specificConfigPath: getFixturePath("broken", "override-conf.yaml"), + getEslintAllConfig, + getEslintRecommendedConfig }); - const targetPath = getFakeFixturePath("overrides", "two", "child-two.js"); + const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); const expected = { + env: { + node: true + }, rules: { - "no-console": [0], - "no-else-return": [0], - "no-unused-vars": [2], - quotes: [2, "double"], - semi: [2, "never"] - } + quotes: [0, "single"], + "no-console": [1] + }, + ignorePatterns: cwdIgnorePatterns }; - const actual = getConfig(factory, targetPath); + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - it("should apply overrides in parent .eslintrc over non-override rules in child .eslintrc", () => { - const targetPath = getFakeFixturePath("overrides", "three", "foo.js"); + // Command line configuration - --rule with --config and first level .eslintrc + it("should merge command line config and rule when rule and config file overrides local .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [ - { - files: "three/**/*.js", - rules: { - "semi-style": [2, "last"] - } - } - ] + cliConfig: { + rules: { + quotes: [1, "double"] + } }, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + specificConfigPath: getFixturePath("broken", "override-conf.yaml"), + getEslintAllConfig, + getEslintRecommendedConfig }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); const expected = { + env: { + node: true + }, rules: { - "semi-style": [2, "last"] - } + quotes: [1, "double"] + }, + ignorePatterns: cwdIgnorePatterns }; - const actual = getConfig(factory, targetPath); + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - it("should apply overrides if all glob patterns match", () => { - const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + // Command line configuration - --plugin + it("should merge command line plugin with local .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [{ - files: ["one/**/*", "*.js"], - rules: { - quotes: [2, "single"] - } - }] + cliConfig: { + plugins: ["another-plugin"] }, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + cwd: getFixturePath("plugins"), + resolvePluginsRelativeTo: getFixturePath("plugins"), + getEslintAllConfig, + getEslintRecommendedConfig }); + const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"); const expected = { + env: { + node: true + }, + plugins: [ + "example", + "another-plugin" + ], rules: { - quotes: [2, "single"] + quotes: [2, "double"] } }; - const actual = getConfig(factory, targetPath); + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - it("should apply overrides even if some glob patterns do not match", () => { - const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + + it("should merge multiple different config file formats", () => { const factory = new CascadingConfigArrayFactory({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [{ - files: ["one/**/*", "*two.js"], - rules: { - quotes: [2, "single"] - } - }] - }, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + getEslintAllConfig, + getEslintRecommendedConfig }); + const file = getFixturePath("fileexts/subdir/subsubdir/foo.js"); const expected = { + env: { + browser: true + }, rules: { - quotes: [2, "single"] - } + semi: [2, "always"], + eqeqeq: [2] + }, + ignorePatterns: cwdIgnorePatterns }; - const actual = getConfig(factory, targetPath); + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - it("should not apply overrides if any excluded glob patterns match", () => { - const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + + it("should load user config globals", () => { + const configPath = path.resolve(dirname, "../fixtures/globals/conf.yaml"); const factory = new CascadingConfigArrayFactory({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [{ - files: "one/**/*", - excludedFiles: ["two/**/*", "*one.js"], - rules: { - quotes: [2, "single"] - } - }] - }, + specificConfigPath: configPath, useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + getEslintAllConfig, + getEslintRecommendedConfig }); const expected = { - rules: {} + globals: { + foo: true + }, + ignorePatterns: cwdIgnorePatterns }; - const actual = getConfig(factory, targetPath); + const actual = getConfig(factory, configPath); assertConfigsEqual(actual, expected); }); - it("should apply overrides if all excluded glob patterns fail to match", () => { - const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + it("should not load disabled environments", () => { + const configPath = path.resolve(dirname, "../fixtures/environments/disable.yaml"); const factory = new CascadingConfigArrayFactory({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [{ - files: "one/**/*", - excludedFiles: ["two/**/*", "*two.js"], - rules: { - quotes: [2, "single"] - } - }] - }, + specificConfigPath: configPath, useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + getEslintAllConfig, + getEslintRecommendedConfig }); - const expected = { - rules: { - quotes: [2, "single"] - } - }; - const actual = getConfig(factory, targetPath); + const config = getConfig(factory, configPath); - assertConfigsEqual(actual, expected); + assert.isUndefined(config.globals.window); }); - it("should cascade", () => { - const targetPath = getFakeFixturePath("overrides", "foo.js"); + it("should gracefully handle empty files", () => { + const configPath = path.resolve(dirname, "../fixtures/configurations/env-node.json"); + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: configPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + getConfig(factory, path.resolve(dirname, "../fixtures/configurations/empty/empty.json")); + }); + + // Meaningful stack-traces + it("should include references to where an `extends` configuration was loaded from", () => { + const configPath = path.resolve(dirname, "../fixtures/config-extends/error.json"); + + assert.throws(() => { + const factory = new CascadingConfigArrayFactory({ + useEslintrc: false, + specificConfigPath: configPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + getConfig(factory, configPath); + }, /Referenced from:.*?error\.json/u); + }); + + // Keep order with the last array element taking highest precedence + it("should make the last element in an array take the highest precedence", () => { + const configPath = path.resolve(dirname, "../fixtures/config-extends/array/.eslintrc"); const factory = new CascadingConfigArrayFactory({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [ - { - files: "foo.js", - rules: { - semi: [2, "never"], - quotes: [2, "single"] - } - }, - { - files: "foo.js", - rules: { - semi: [2, "never"], - quotes: [2, "double"] - } - } - ] - }, useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + specificConfigPath: configPath, + getEslintAllConfig, + getEslintRecommendedConfig }); const expected = { - rules: { - semi: [2, "never"], - quotes: [2, "double"] - } + rules: { "no-empty": [1], "comma-dangle": [2], "no-console": [2] }, + env: { browser: false, node: true, es6: true }, + ignorePatterns: cwdIgnorePatterns }; - const actual = getConfig(factory, targetPath); + const actual = getConfig(factory, configPath); assertConfigsEqual(actual, expected); }); - }); - describe("deprecation warnings", () => { - const cwd = path.resolve(dirname, "../fixtures/config-file/"); - let warning = null; + describe("with env in a child configuration file", () => { + it("should not overwrite parserOptions of the parent with env of the child", () => { + const factory = new CascadingConfigArrayFactory({ + getEslintAllConfig, + getEslintRecommendedConfig + }); + const targetPath = getFixturePath("overwrite-ecmaFeatures", "child", "foo.js"); + const expected = { + rules: {}, + env: { commonjs: true }, + parserOptions: { ecmaFeatures: { globalReturn: false } }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + }); - /** - * Store a reported warning object if that code starts with `ESLINT_`. - * @param {{code:string, message:string}} w The warning object to store. - * @returns {void} - */ - function onWarning(w) { - if (w.code.startsWith("ESLINT_")) { - warning = w; + describe("personal config file within home directory", () => { + + const root = path.join(systemTempDir, "eslint/cli-engine/cascading-config-array-factory/personal-config"); + + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root, + files: { + ...DIRECTORY_CONFIG_HIERARCHY + } + }); + + before(prepare); + after(cleanup); + + /** + * Returns the path inside of the fixture directory. + * @param {...string} args file path segments. + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFakeFixturePath(...args) { + return path.join(getPath(), "eslint", "fixtures", "config-hierarchy", ...args); } - } - /** @type {CascadingConfigArrayFactory} */ - let factory; + it("should load the personal config if no local config was found", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "home-folder"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + mockOsHomedir(homePath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + "home-folder-rule": [2] + } + }; - beforeEach(() => { - factory = new CascadingConfigArrayFactory({ - cwd, - eslintAllPath, - eslintRecommendedPath + assertConfigsEqual(actual, expected); + }); + + it("should ignore the personal config if a local config was found", () => { + const projectPath = getFakeFixturePath("personal-config", "home-folder", "project"); + const homePath = getFakeFixturePath("personal-config", "home-folder"); + const filePath = getFakeFixturePath("personal-config", "home-folder", "project", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + mockOsHomedir(homePath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + "project-level-rule": [2] + } + }; + + assertConfigsEqual(actual, expected); + }); + + it("should ignore the personal config if config is passed through cli", () => { + const configPath = getFakeFixturePath("quotes-error.json"); + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "home-folder"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + specificConfigPath: configPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + mockOsHomedir(homePath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + quotes: [2, "double"] + } + }; + + assertConfigsEqual(actual, expected); + }); + + it("should still load the project config if the current working directory is the same as the home folder", () => { + const projectPath = getFakeFixturePath("personal-config", "project-with-config"); + const filePath = getFakeFixturePath("personal-config", "project-with-config", "subfolder", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + mockOsHomedir(projectPath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + "project-level-rule": [2], + "subfolder-level-rule": [2] + } + }; + + assertConfigsEqual(actual, expected); }); - warning = null; - process.on("warning", onWarning); }); - afterEach(() => { - process.removeListener("warning", onWarning); + + describe("when no local or personal config is found", () => { + + const root = path.join(systemTempDir, "eslint/cli-engine/cascading-config-array-factory/personal-config"); + + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root, + files: { + ...DIRECTORY_CONFIG_HIERARCHY + } + }); + + before(prepare); + after(cleanup); + + /** + * Returns the path inside of the fixture directory. + * @param {...string} args file path segments. + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFakeFixturePath(...args) { + return path.join(getPath(), "eslint", "fixtures", "config-hierarchy", ...args); + } + + it("should throw an error if no local config and no personal config was found", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + mockOsHomedir(homePath); + + assert.throws(() => { + getConfig(factory, filePath); + }, "No ESLint configuration found"); + }); + + it("should throw an error if no local config was found and ~/package.json contains no eslintConfig section", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "home-folder-with-packagejson"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + mockOsHomedir(homePath); + + assert.throws(() => { + getConfig(factory, filePath); + }, "No ESLint configuration found"); + }); + + it("should not throw an error if no local config and no personal config was found but useEslintrc is false", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + mockOsHomedir(homePath); + + getConfig(factory, filePath); + }); + + it("should not throw an error if no local config and no personal config was found but rules are specified", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cliConfig: { + rules: { quotes: [2, "single"] } + }, + cwd: projectPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + mockOsHomedir(homePath); + + getConfig(factory, filePath); + }); + + it("should not throw an error if no local config and no personal config was found but baseConfig is specified", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + baseConfig: {}, + cwd: projectPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + mockOsHomedir(homePath); + + getConfig(factory, filePath); + }); }); - it("should emit a deprecation warning if 'ecmaFeatures' is given.", async () => { - getConfig(factory, "ecma-features/test.js"); + describe("with overrides", () => { - // Wait for "warning" event. - await nextTick(); + const root = path.join(systemTempDir, "eslint/cli-engine/cascading-config-array-factory/personal-config"); - assert.notStrictEqual(warning, null); - assert.strictEqual( - warning.message, - `The 'ecmaFeatures' config file property is deprecated and has no effect. (found in "ecma-features${path.sep}.eslintrc.yml")` - ); + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root, + files: { + ...DIRECTORY_CONFIG_HIERARCHY + } + }); + + before(prepare); + after(cleanup); + + /** + * Returns the path inside of the fixture directory. + * @param {...string} pathSegments One or more path segments, in order of depth, shallowest first + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFakeFixturePath(...pathSegments) { + return path.join(getPath(), "eslint", "fixtures", "config-hierarchy", ...pathSegments); + } + + it("should merge override config when the pattern matches the file name", () => { + const factory = new CascadingConfigArrayFactory({ + cwd: getPath(), + getEslintAllConfig, + getEslintRecommendedConfig + }); + const targetPath = getFakeFixturePath("overrides", "foo.js"); + const expected = { + rules: { + quotes: [2, "single"], + "no-else-return": [0], + "no-unused-vars": [1], + semi: [1, "never"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should merge override config when the pattern matches the file path relative to the config file", () => { + const factory = new CascadingConfigArrayFactory({ + cwd: getPath(), + getEslintAllConfig, + getEslintRecommendedConfig + }); + const targetPath = getFakeFixturePath("overrides", "child", "child-one.js"); + const expected = { + rules: { + curly: ["error", "multi", "consistent"], + "no-else-return": [0], + "no-unused-vars": [1], + quotes: [2, "double"], + semi: [1, "never"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should not merge override config when the pattern matches the absolute file path", () => { + const resolvedPath = path.resolve(dirname, "../fixtures/config-hierarchy/overrides/bar.cjs"); + + assert.throws(() => new CascadingConfigArrayFactory({ + cwd: getPath(), + baseConfig: { + overrides: [{ + files: resolvedPath, + rules: { + quotes: [1, "double"] + } + }] + }, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }), /Invalid override pattern/u); + }); + + it("should not merge override config when the pattern traverses up the directory tree", () => { + const parentPath = "overrides/../**/*.js"; + + assert.throws(() => new CascadingConfigArrayFactory({ + baseConfig: { + overrides: [{ + files: parentPath, + rules: { + quotes: [1, "single"] + } + }] + }, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }), /Invalid override pattern/u); + }); + + it("should merge all local configs (override and non-override) before non-local configs", () => { + const factory = new CascadingConfigArrayFactory({ + cwd: getPath(), + getEslintAllConfig, + getEslintRecommendedConfig + }); + const targetPath = getFakeFixturePath("overrides", "two", "child-two.js"); + const expected = { + rules: { + "no-console": [0], + "no-else-return": [0], + "no-unused-vars": [2], + quotes: [2, "double"], + semi: [2, "never"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides in parent .eslintrc over non-override rules in child .eslintrc", () => { + const targetPath = getFakeFixturePath("overrides", "three", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [ + { + files: "three/**/*.js", + rules: { + "semi-style": [2, "last"] + } + } + ] + }, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + const expected = { + rules: { + "semi-style": [2, "last"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides if all glob patterns match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: ["one/**/*", "*.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + const expected = { + rules: { + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides even if some glob patterns do not match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: ["one/**/*", "*two.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + const expected = { + rules: { + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should not apply overrides if any excluded glob patterns match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: "one/**/*", + excludedFiles: ["two/**/*", "*one.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + const expected = { + rules: {} + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides if all excluded glob patterns fail to match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: "one/**/*", + excludedFiles: ["two/**/*", "*two.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + const expected = { + rules: { + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should cascade", () => { + const targetPath = getFakeFixturePath("overrides", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [ + { + files: "foo.js", + rules: { + semi: [2, "never"], + quotes: [2, "single"] + } + }, + { + files: "foo.js", + rules: { + semi: [2, "never"], + quotes: [2, "double"] + } + } + ] + }, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + const expected = { + rules: { + semi: [2, "never"], + quotes: [2, "double"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); }); }); }); @@ -1943,66 +3148,134 @@ describe("CascadingConfigArrayFactory", () => { }; const { prepare, cleanup, getPath } = createCustomTeardown({ cwd: root, files }); - /** @type {Map} */ - let additionalPluginPool; + describe("with eslint built-in config paths", () => { - /** @type {CascadingConfigArrayFactory} */ - let factory; + /** @type {Map} */ + let additionalPluginPool; - beforeEach(async () => { - await prepare(); - additionalPluginPool = new Map(); - factory = new CascadingConfigArrayFactory({ - cwd: getPath(), - additionalPluginPool, - cliConfig: { plugins: ["test"] }, - eslintAllPath, - eslintRecommendedPath + /** @type {CascadingConfigArrayFactory} */ + let factory; + + beforeEach(async () => { + await prepare(); + additionalPluginPool = new Map(); + factory = new CascadingConfigArrayFactory({ + cwd: getPath(), + additionalPluginPool, + cliConfig: { plugins: ["test"] }, + eslintAllPath, + eslintRecommendedPath + }); }); - }); - afterEach(cleanup); + afterEach(cleanup); - it("should use cached instance.", () => { - const one = factory.getConfigArrayForFile("a.js"); - const two = factory.getConfigArrayForFile("a.js"); + it("should use cached instance.", () => { + const one = factory.getConfigArrayForFile("a.js"); + const two = factory.getConfigArrayForFile("a.js"); - assert.strictEqual(one, two); - }); + assert.strictEqual(one, two); + }); - it("should not use cached instance if 'clearCache()' method is called after first config is retrieved", () => { - const one = factory.getConfigArrayForFile("a.js"); + it("should not use cached instance if 'clearCache()' method is called after first config is retrieved", () => { + const one = factory.getConfigArrayForFile("a.js"); - factory.clearCache(); - const two = factory.getConfigArrayForFile("a.js"); + factory.clearCache(); + const two = factory.getConfigArrayForFile("a.js"); - assert.notStrictEqual(one, two); - }); + assert.notStrictEqual(one, two); + }); + + it("should have a loading error in CLI config.", () => { + const config = factory.getConfigArrayForFile("a.js"); + + assert.strictEqual(config[2].plugins.test.definition, null); + }); - it("should have a loading error in CLI config.", () => { - const config = factory.getConfigArrayForFile("a.js"); + it("should not have a loading error in CLI config after adding 'test' plugin to the additional plugin pool then calling 'clearCache()'.", () => { + factory.getConfigArrayForFile("a.js"); - assert.strictEqual(config[2].plugins.test.definition, null); + additionalPluginPool.set("test", { configs: { name: "test" } }); + factory.clearCache(); + + // Check. + const config = factory.getConfigArrayForFile("a.js"); + + assert.deepStrictEqual( + config[2].plugins.test.definition, + { + configs: { name: "test" }, + environments: {}, + processors: {}, + rules: {} + } + ); + }); }); - it("should not have a loading error in CLI config after adding 'test' plugin to the additional plugin pool then calling 'clearCache()'.", () => { - factory.getConfigArrayForFile("a.js"); + describe("with eslint built-in config callbacks", () => { - additionalPluginPool.set("test", { configs: { name: "test" } }); - factory.clearCache(); + /** @type {Map} */ + let additionalPluginPool; - // Check. - const config = factory.getConfigArrayForFile("a.js"); + /** @type {CascadingConfigArrayFactory} */ + let factory; - assert.deepStrictEqual( - config[2].plugins.test.definition, - { - configs: { name: "test" }, - environments: {}, - processors: {}, - rules: {} - } - ); + beforeEach(async () => { + await prepare(); + additionalPluginPool = new Map(); + factory = new CascadingConfigArrayFactory({ + cwd: getPath(), + additionalPluginPool, + cliConfig: { plugins: ["test"] }, + getEslintAllConfig, + getEslintRecommendedConfig + }); + }); + + afterEach(cleanup); + + it("should use cached instance.", () => { + const one = factory.getConfigArrayForFile("a.js"); + const two = factory.getConfigArrayForFile("a.js"); + + assert.strictEqual(one, two); + }); + + it("should not use cached instance if 'clearCache()' method is called after first config is retrieved", () => { + const one = factory.getConfigArrayForFile("a.js"); + + factory.clearCache(); + const two = factory.getConfigArrayForFile("a.js"); + + assert.notStrictEqual(one, two); + }); + + it("should have a loading error in CLI config.", () => { + const config = factory.getConfigArrayForFile("a.js"); + + assert.strictEqual(config[2].plugins.test.definition, null); + }); + + it("should not have a loading error in CLI config after adding 'test' plugin to the additional plugin pool then calling 'clearCache()'.", () => { + factory.getConfigArrayForFile("a.js"); + + additionalPluginPool.set("test", { configs: { name: "test" } }); + factory.clearCache(); + + // Check. + const config = factory.getConfigArrayForFile("a.js"); + + assert.deepStrictEqual( + config[2].plugins.test.definition, + { + configs: { name: "test" }, + environments: {}, + processors: {}, + rules: {} + } + ); + }); }); }); }); diff --git a/tests/lib/config-array-factory.js b/tests/lib/config-array-factory.js index 805159fd..6d9fc2c8 100644 --- a/tests/lib/config-array-factory.js +++ b/tests/lib/config-array-factory.js @@ -7,15 +7,16 @@ // Requirements //----------------------------------------------------------------------------- -import path from "path"; -import { fileURLToPath, pathToFileURL } from "url"; +import { assert } from "chai"; import fs from "fs"; import { createRequire } from "module"; -import { assert } from "chai"; +import path from "path"; import sinon from "sinon"; +import systemTempDir from "temp-dir"; +import { fileURLToPath } from "url"; + import { Legacy } from "../../lib/index.js"; import { createCustomTeardown } from "../_utils/index.js"; -import systemTempDir from "temp-dir"; const require = createRequire(import.meta.url); @@ -38,6 +39,22 @@ const eslintAllPath = path.resolve(dirname, "../fixtures/eslint-all.cjs"); const eslintRecommendedPath = path.resolve(dirname, "../fixtures/eslint-recommended.cjs"); const tempDir = path.join(systemTempDir, "eslintrc/config-array-factory"); +/** + * Return config data for built-in eslint:all. + * @returns {ConfigData} Config data + */ +function getEslintAllConfig() { + return require("../fixtures/eslint-all.cjs"); +} + +/** + * Return config data for built-in eslint:recommended. + * @returns {ConfigData} Config data + */ +function getEslintRecommendedConfig() { + return require("../fixtures/eslint-recommended.cjs"); +} + /** * Assert a config array element. * @param {Object} actual The actual value. @@ -924,328 +941,646 @@ describe("ConfigArrayFactory", () => { }); describe("'extends' details", () => { + describe("'with eslint built-in config paths", () => { + let prepare, cleanup, getPath; + + before(() => { + + ({ prepare, cleanup, getPath } = createCustomTeardown({ + cwd: tempDir, + files: { + "node_modules/eslint-config-foo/index.js": "exports.env = { browser: true }", + "node_modules/eslint-config-one/index.js": "module.exports = { extends: 'two', env: { browser: true } }", + "node_modules/eslint-config-two/index.js": "module.exports = { env: { node: true } }", + "node_modules/eslint-config-override/index.js": ` + module.exports = { + rules: { regular: 1 }, + overrides: [ + { files: '*.xxx', rules: { override: 1 } }, + { files: '*.yyy', rules: { override: 2 } } + ] + } + `, + "node_modules/eslint-plugin-foo/index.js": "exports.configs = { bar: { env: { es6: true } } }", + "node_modules/eslint-plugin-invalid-config/index.js": "exports.configs = { foo: {} }", + "node_modules/eslint-plugin-error/index.js": "throw new Error('xxx error')", + "base.js": "module.exports = { rules: { semi: [2, 'always'] } };" + } + })); - let prepare, cleanup, getPath; + factory = new ConfigArrayFactory({ + cwd: getPath(), + eslintAllPath, + eslintRecommendedPath + }); + }); - before(() => { + beforeEach(() => prepare()); + afterEach(() => cleanup()); - ({ prepare, cleanup, getPath } = createCustomTeardown({ - cwd: tempDir, - files: { - "node_modules/eslint-config-foo/index.js": "exports.env = { browser: true }", - "node_modules/eslint-config-one/index.js": "module.exports = { extends: 'two', env: { browser: true } }", - "node_modules/eslint-config-two/index.js": "module.exports = { env: { node: true } }", - "node_modules/eslint-config-override/index.js": ` - module.exports = { - rules: { regular: 1 }, - overrides: [ - { files: '*.xxx', rules: { override: 1 } }, - { files: '*.yyy', rules: { override: 2 } } - ] - } - `, - "node_modules/eslint-plugin-foo/index.js": "exports.configs = { bar: { env: { es6: true } } }", - "node_modules/eslint-plugin-invalid-config/index.js": "exports.configs = { foo: {} }", - "node_modules/eslint-plugin-error/index.js": "throw new Error('xxx error')", - "base.js": "module.exports = { rules: { semi: [2, 'always'] } };" - } - })); + it("should throw an error when extends config module is not found", () => { + assert.throws(() => { + create({ + extends: "not-exist", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "not-exist" to extend from./u); + }); - factory = new ConfigArrayFactory({ - cwd: getPath(), - eslintAllPath, - eslintRecommendedPath + it("should throw an error when an eslint config is not found", () => { + assert.throws(() => { + create({ + extends: "eslint:foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "eslint:foo" to extend from./u); }); - }); - beforeEach(() => prepare()); - afterEach(() => cleanup()); + it("should throw an error when a plugin threw while loading.", () => { + assert.throws(() => { + create({ + extends: "plugin:error/foo", + rules: { eqeqeq: 2 } + }); + }, /xxx error/u); + }); - it("should throw an error when extends config module is not found", () => { - assert.throws(() => { - create({ - extends: "not-exist", - rules: { eqeqeq: 2 } + it("should throw an error when a plugin extend is a file path.", () => { + assert.throws(() => { + create({ + extends: "plugin:./path/to/foo", + rules: { eqeqeq: 2 } + }); + }, /'extends' cannot use a file path for plugins/u); + }); + + it("should throw an error when an eslint config is not found", () => { + assert.throws(() => { + create({ + extends: "eslint:foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "eslint:foo" to extend from./u); + }); + + describe("if 'extends' property was 'eslint:all', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "eslint:all", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); }); - }, /Failed to load config "not-exist" to extend from./u); - }); - it("should throw an error when an eslint config is not found", () => { - assert.throws(() => { - create({ - extends: "eslint:foo", - rules: { eqeqeq: 2 } + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); }); - }, /Failed to load config "eslint:foo" to extend from./u); - }); - it("should throw an error when a plugin threw while loading.", () => { - assert.throws(() => { - create({ - extends: "plugin:error/foo", - rules: { eqeqeq: 2 } + it("should have the config data of 'eslint:all' at the first element.", async () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint:all", + filePath: eslintAllPath, + ...getEslintAllConfig() + }); }); - }, /xxx error/u); - }); - it("should throw an error when a plugin extend is a file path.", () => { - assert.throws(() => { - create({ - extends: "plugin:./path/to/foo", - rules: { eqeqeq: 2 } + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); }); - }, /'extends' cannot use a file path for plugins/u); - }); + }); - it("should throw an error when an eslint config is not found", () => { - assert.throws(() => { - create({ - extends: "eslint:foo", - rules: { eqeqeq: 2 } + describe("if 'extends' property was 'eslint:recommended', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "eslint:recommended", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); }); - }, /Failed to load config "eslint:foo" to extend from./u); - }); - describe("if 'extends' property was 'eslint:all', the returned value", () => { - let configArray; + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); + }); - beforeEach(() => { - configArray = create( - { extends: "eslint:all", rules: { eqeqeq: 1 } }, - { name: ".eslintrc" } - ); - }); + it("should have the config data of 'eslint:recommended' at the first element.", async () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint:recommended", + filePath: eslintRecommendedPath, + ...getEslintRecommendedConfig() + }); + }); - it("should have two elements.", () => { - assert.strictEqual(configArray.length, 2); + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); }); - it("should have the config data of 'eslint:all' at the first element.", async () => { - assertConfigArrayElement(configArray[0], { - name: ".eslintrc » eslint:all", - filePath: eslintAllPath, - ...(await import(pathToFileURL(eslintAllPath))).default + describe("if 'extends' property was 'foo', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "foo", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); }); - }); - it("should have the given config data at the second element.", () => { - assertConfigArrayElement(configArray[1], { - name: ".eslintrc", - rules: { eqeqeq: 1 } + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); }); - }); - }); - describe("if 'extends' property was 'eslint:recommended', the returned value", () => { - let configArray; + it("should have the config data of 'eslint-config-foo' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint-config-foo", + filePath: path.join(getPath(), "node_modules/eslint-config-foo/index.js"), + env: { browser: true } + }); + }); - beforeEach(() => { - configArray = create( - { extends: "eslint:recommended", rules: { eqeqeq: 1 } }, - { name: ".eslintrc" } - ); + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); }); - it("should have two elements.", () => { - assert.strictEqual(configArray.length, 2); - }); + describe("if 'extends' property was 'plugin:foo/bar', the returned value", () => { + let configArray; - it("should have the config data of 'eslint:recommended' at the first element.", async () => { - assertConfigArrayElement(configArray[0], { - name: ".eslintrc » eslint:recommended", - filePath: eslintRecommendedPath, - ...(await import(pathToFileURL(eslintRecommendedPath))).default + beforeEach(() => { + configArray = create( + { extends: "plugin:foo/bar", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); }); - }); - it("should have the given config data at the second element.", () => { - assertConfigArrayElement(configArray[1], { - name: ".eslintrc", - rules: { eqeqeq: 1 } + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); }); - }); - }); - describe("if 'extends' property was 'foo', the returned value", () => { - let configArray; + it("should have the config data of 'plugin:foo/bar' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » plugin:foo/bar", + filePath: path.join(getPath(), "node_modules/eslint-plugin-foo/index.js"), + env: { es6: true } + }); + }); - beforeEach(() => { - configArray = create( - { extends: "foo", rules: { eqeqeq: 1 } }, - { name: ".eslintrc" } - ); + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); }); - it("should have two elements.", () => { - assert.strictEqual(configArray.length, 2); - }); + describe("if 'extends' property was './base', the returned value", () => { + let configArray; - it("should have the config data of 'eslint-config-foo' at the first element.", () => { - assertConfigArrayElement(configArray[0], { - name: ".eslintrc » eslint-config-foo", - filePath: path.join(getPath(), "node_modules/eslint-config-foo/index.js"), - env: { browser: true } + beforeEach(() => { + configArray = create( + { extends: "./base", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); }); - }); - it("should have the given config data at the second element.", () => { - assertConfigArrayElement(configArray[1], { - name: ".eslintrc", - rules: { eqeqeq: 1 } + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); }); - }); - }); - describe("if 'extends' property was 'plugin:foo/bar', the returned value", () => { - let configArray; + it("should have the config data of './base' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » ./base", + filePath: path.join(getPath(), "base.js"), + rules: { semi: [2, "always"] } + }); + }); - beforeEach(() => { - configArray = create( - { extends: "plugin:foo/bar", rules: { eqeqeq: 1 } }, - { name: ".eslintrc" } - ); + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); }); - it("should have two elements.", () => { - assert.strictEqual(configArray.length, 2); - }); + describe("if 'extends' property was 'one' and the 'one' extends 'two', the returned value", () => { + let configArray; - it("should have the config data of 'plugin:foo/bar' at the first element.", () => { - assertConfigArrayElement(configArray[0], { - name: ".eslintrc » plugin:foo/bar", - filePath: path.join(getPath(), "node_modules/eslint-plugin-foo/index.js"), - env: { es6: true } + beforeEach(() => { + configArray = create( + { extends: "one", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); }); - }); - it("should have the given config data at the second element.", () => { - assertConfigArrayElement(configArray[1], { - name: ".eslintrc", - rules: { eqeqeq: 1 } + it("should have three elements.", () => { + assert.strictEqual(configArray.length, 3); }); - }); - }); - describe("if 'extends' property was './base', the returned value", () => { - let configArray; + it("should have the config data of 'eslint-config-two' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint-config-one » eslint-config-two", + filePath: path.join(getPath(), "node_modules/eslint-config-two/index.js"), + env: { node: true } + }); + }); - beforeEach(() => { - configArray = create( - { extends: "./base", rules: { eqeqeq: 1 } }, - { name: ".eslintrc" } - ); - }); + it("should have the config data of 'eslint-config-one' at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc » eslint-config-one", + filePath: path.join(getPath(), "node_modules/eslint-config-one/index.js"), + env: { browser: true } + }); + }); - it("should have two elements.", () => { - assert.strictEqual(configArray.length, 2); + it("should have the given config data at the third element.", () => { + assertConfigArrayElement(configArray[2], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); }); - it("should have the config data of './base' at the first element.", () => { - assertConfigArrayElement(configArray[0], { - name: ".eslintrc » ./base", - filePath: path.join(getPath(), "base.js"), - rules: { semi: [2, "always"] } + describe("if 'extends' property was 'override' and the 'override' has 'overrides' property, the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "override", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); }); - }); - it("should have the given config data at the second element.", () => { - assertConfigArrayElement(configArray[1], { - name: ".eslintrc", - rules: { eqeqeq: 1 } + it("should have four elements.", () => { + assert.strictEqual(configArray.length, 4); + }); + + it("should have the config data of 'eslint-config-override' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint-config-override", + filePath: path.join(getPath(), "node_modules/eslint-config-override/index.js"), + rules: { regular: 1 } + }); + }); + + it("should have the 'overrides[0]' config data of 'eslint-config-override' at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc » eslint-config-override#overrides[0]", + filePath: path.join(getPath(), "node_modules/eslint-config-override/index.js"), + criteria: OverrideTester.create(["*.xxx"], [], getPath()), + rules: { override: 1 } + }); + }); + + it("should have the 'overrides[1]' config data of 'eslint-config-override' at the third element.", () => { + assertConfigArrayElement(configArray[2], { + name: ".eslintrc » eslint-config-override#overrides[1]", + filePath: path.join(getPath(), "node_modules/eslint-config-override/index.js"), + criteria: OverrideTester.create(["*.yyy"], [], tempDir), + rules: { override: 2 } + }); + }); + + it("should have the given config data at the fourth element.", () => { + assertConfigArrayElement(configArray[3], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); }); }); }); - describe("if 'extends' property was 'one' and the 'one' extends 'two', the returned value", () => { - let configArray; + describe("'with eslint built-in config callbacks", () => { + let prepare, cleanup, getPath; - beforeEach(() => { - configArray = create( - { extends: "one", rules: { eqeqeq: 1 } }, - { name: ".eslintrc" } - ); - }); + before(() => { - it("should have three elements.", () => { - assert.strictEqual(configArray.length, 3); - }); + ({ prepare, cleanup, getPath } = createCustomTeardown({ + cwd: tempDir, + files: { + "node_modules/eslint-config-foo/index.js": "exports.env = { browser: true }", + "node_modules/eslint-config-one/index.js": "module.exports = { extends: 'two', env: { browser: true } }", + "node_modules/eslint-config-two/index.js": "module.exports = { env: { node: true } }", + "node_modules/eslint-config-override/index.js": ` + module.exports = { + rules: { regular: 1 }, + overrides: [ + { files: '*.xxx', rules: { override: 1 } }, + { files: '*.yyy', rules: { override: 2 } } + ] + } + `, + "node_modules/eslint-plugin-foo/index.js": "exports.configs = { bar: { env: { es6: true } } }", + "node_modules/eslint-plugin-invalid-config/index.js": "exports.configs = { foo: {} }", + "node_modules/eslint-plugin-error/index.js": "throw new Error('xxx error')", + "base.js": "module.exports = { rules: { semi: [2, 'always'] } };" + } + })); - it("should have the config data of 'eslint-config-two' at the first element.", () => { - assertConfigArrayElement(configArray[0], { - name: ".eslintrc » eslint-config-one » eslint-config-two", - filePath: path.join(getPath(), "node_modules/eslint-config-two/index.js"), - env: { node: true } + factory = new ConfigArrayFactory({ + cwd: getPath(), + getEslintAllConfig, + getEslintRecommendedConfig }); }); - it("should have the config data of 'eslint-config-one' at the second element.", () => { - assertConfigArrayElement(configArray[1], { - name: ".eslintrc » eslint-config-one", - filePath: path.join(getPath(), "node_modules/eslint-config-one/index.js"), - env: { browser: true } - }); + beforeEach(() => prepare()); + afterEach(() => cleanup()); + + it("should throw an error when extends config module is not found", () => { + assert.throws(() => { + create({ + extends: "not-exist", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "not-exist" to extend from./u); }); - it("should have the given config data at the third element.", () => { - assertConfigArrayElement(configArray[2], { - name: ".eslintrc", - rules: { eqeqeq: 1 } - }); + it("should throw an error when an eslint config is not found", () => { + assert.throws(() => { + create({ + extends: "eslint:foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "eslint:foo" to extend from./u); }); - }); - describe("if 'extends' property was 'override' and the 'override' has 'overrides' property, the returned value", () => { - let configArray; + it("should throw an error when a plugin threw while loading.", () => { + assert.throws(() => { + create({ + extends: "plugin:error/foo", + rules: { eqeqeq: 2 } + }); + }, /xxx error/u); + }); - beforeEach(() => { - configArray = create( - { extends: "override", rules: { eqeqeq: 1 } }, - { name: ".eslintrc" } - ); + it("should throw an error when a plugin extend is a file path.", () => { + assert.throws(() => { + create({ + extends: "plugin:./path/to/foo", + rules: { eqeqeq: 2 } + }); + }, /'extends' cannot use a file path for plugins/u); }); - it("should have four elements.", () => { - assert.strictEqual(configArray.length, 4); + it("should throw an error when an eslint config is not found", () => { + assert.throws(() => { + create({ + extends: "eslint:foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "eslint:foo" to extend from./u); }); - it("should have the config data of 'eslint-config-override' at the first element.", () => { - assertConfigArrayElement(configArray[0], { - name: ".eslintrc » eslint-config-override", - filePath: path.join(getPath(), "node_modules/eslint-config-override/index.js"), - rules: { regular: 1 } + describe("if 'extends' property was 'eslint:all', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "eslint:all", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); }); - }); - it("should have the 'overrides[0]' config data of 'eslint-config-override' at the second element.", () => { - assertConfigArrayElement(configArray[1], { - name: ".eslintrc » eslint-config-override#overrides[0]", - filePath: path.join(getPath(), "node_modules/eslint-config-override/index.js"), - criteria: OverrideTester.create(["*.xxx"], [], getPath()), - rules: { override: 1 } + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); + }); + + it("should have the config data of 'eslint:all' at the first element.", async () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint:all", + ...getEslintAllConfig() + }); + }); + + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); }); }); - it("should have the 'overrides[1]' config data of 'eslint-config-override' at the third element.", () => { - assertConfigArrayElement(configArray[2], { - name: ".eslintrc » eslint-config-override#overrides[1]", - filePath: path.join(getPath(), "node_modules/eslint-config-override/index.js"), - criteria: OverrideTester.create(["*.yyy"], [], tempDir), - rules: { override: 2 } + describe("if 'extends' property was 'eslint:recommended', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "eslint:recommended", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); + }); + + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); + }); + + it("should have the config data of 'eslint:recommended' at the first element.", async () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint:recommended", + ...getEslintRecommendedConfig() + }); + }); + + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); }); }); - it("should have the given config data at the fourth element.", () => { - assertConfigArrayElement(configArray[3], { - name: ".eslintrc", - rules: { eqeqeq: 1 } + describe("if 'extends' property was 'foo', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "foo", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); + }); + + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); + }); + + it("should have the config data of 'eslint-config-foo' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint-config-foo", + filePath: path.join(getPath(), "node_modules/eslint-config-foo/index.js"), + env: { browser: true } + }); + }); + + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); }); }); - }); - }); - describe("'overrides' details", () => { + describe("if 'extends' property was 'plugin:foo/bar', the returned value", () => { + let configArray; - const { prepare, cleanup, getPath } = createCustomTeardown({ + beforeEach(() => { + configArray = create( + { extends: "plugin:foo/bar", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); + }); + + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); + }); + + it("should have the config data of 'plugin:foo/bar' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » plugin:foo/bar", + filePath: path.join(getPath(), "node_modules/eslint-plugin-foo/index.js"), + env: { es6: true } + }); + }); + + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); + }); + + describe("if 'extends' property was './base', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "./base", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); + }); + + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); + }); + + it("should have the config data of './base' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » ./base", + filePath: path.join(getPath(), "base.js"), + rules: { semi: [2, "always"] } + }); + }); + + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); + }); + + describe("if 'extends' property was 'one' and the 'one' extends 'two', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "one", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); + }); + + it("should have three elements.", () => { + assert.strictEqual(configArray.length, 3); + }); + + it("should have the config data of 'eslint-config-two' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint-config-one » eslint-config-two", + filePath: path.join(getPath(), "node_modules/eslint-config-two/index.js"), + env: { node: true } + }); + }); + + it("should have the config data of 'eslint-config-one' at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc » eslint-config-one", + filePath: path.join(getPath(), "node_modules/eslint-config-one/index.js"), + env: { browser: true } + }); + }); + + it("should have the given config data at the third element.", () => { + assertConfigArrayElement(configArray[2], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); + }); + + describe("if 'extends' property was 'override' and the 'override' has 'overrides' property, the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "override", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); + }); + + it("should have four elements.", () => { + assert.strictEqual(configArray.length, 4); + }); + + it("should have the config data of 'eslint-config-override' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint-config-override", + filePath: path.join(getPath(), "node_modules/eslint-config-override/index.js"), + rules: { regular: 1 } + }); + }); + + it("should have the 'overrides[0]' config data of 'eslint-config-override' at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc » eslint-config-override#overrides[0]", + filePath: path.join(getPath(), "node_modules/eslint-config-override/index.js"), + criteria: OverrideTester.create(["*.xxx"], [], getPath()), + rules: { override: 1 } + }); + }); + + it("should have the 'overrides[1]' config data of 'eslint-config-override' at the third element.", () => { + assertConfigArrayElement(configArray[2], { + name: ".eslintrc » eslint-config-override#overrides[1]", + filePath: path.join(getPath(), "node_modules/eslint-config-override/index.js"), + criteria: OverrideTester.create(["*.yyy"], [], tempDir), + rules: { override: 2 } + }); + }); + + it("should have the given config data at the fourth element.", () => { + assertConfigArrayElement(configArray[3], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); + }); + }); + }); + + describe("'overrides' details", () => { + + const { prepare, cleanup, getPath } = createCustomTeardown({ cwd: tempDir, files: { "node_modules/eslint-config-foo/index.js": ` @@ -1588,217 +1923,436 @@ describe("ConfigArrayFactory", () => { "yaml/.eslintrc.yaml": "env:\n browser: true" }; const { prepare, cleanup, getPath } = createCustomTeardown({ cwd: tempDir, files }); - let factory; - beforeEach(async () => { - await prepare(); - factory = new ConfigArrayFactory({ - cwd: getPath(), - eslintAllPath, - eslintRecommendedPath + describe("with eslint built-in config paths", () => { + let factory; + + beforeEach(async () => { + await prepare(); + factory = new ConfigArrayFactory({ + cwd: getPath(), + eslintAllPath, + eslintRecommendedPath + }); }); - }); - afterEach(cleanup); + afterEach(cleanup); - /** - * Apply `extends` property. - * @param {Object} configData The config that has `extends` property. - * @param {string} [filePath] The path to the config data. - * @returns {Object} The applied config data. - */ - function applyExtends(configData, filePath = "whatever") { - return factory - .create(configData, { filePath }) - .extractConfig(filePath) - .toCompatibleObjectAsConfigFileContent(); - } + /** + * Apply `extends` property. + * @param {Object} configData The config that has `extends` property. + * @param {string} [filePath] The path to the config data. + * @returns {Object} The applied config data. + */ + function applyExtends(configData, filePath = "whatever") { + return factory + .create(configData, { filePath }) + .extractConfig(filePath) + .toCompatibleObjectAsConfigFileContent(); + } - it("should apply extension 'foo' when specified from root directory config", () => { - const config = applyExtends({ - extends: "foo", - rules: { eqeqeq: 2 } + it("should apply extension 'foo' when specified from root directory config", () => { + const config = applyExtends({ + extends: "foo", + rules: { eqeqeq: 2 } + }); + + assertConfig(config, { + env: { browser: true }, + rules: { eqeqeq: [2] } + }); }); - assertConfig(config, { - env: { browser: true }, - rules: { eqeqeq: [2] } + it("should apply all rules when extends config includes 'eslint:all'", () => { + const config = applyExtends({ + extends: "eslint:all" + }); + + assert.strictEqual(config.rules.eqeqeq[0], "error"); + assert.strictEqual(config.rules.curly[0], "error"); }); - }); - it("should apply all rules when extends config includes 'eslint:all'", () => { - const config = applyExtends({ - extends: "eslint:all" + it("should throw an error when extends config module is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "not-exist", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "not-exist" to extend from./u); }); - assert.strictEqual(config.rules.eqeqeq[0], "error"); - assert.strictEqual(config.rules.curly[0], "error"); - }); + it("should throw an error when an eslint config is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "eslint:foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "eslint:foo" to extend from./u); + }); - it("should throw an error when extends config module is not found", () => { - assert.throws(() => { - applyExtends({ - extends: "not-exist", - rules: { eqeqeq: 2 } - }); - }, /Failed to load config "not-exist" to extend from./u); - }); + it("should throw an error when a parser in a plugin config is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "plugin:invalid-parser/foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load parser 'nonexistent-parser' declared in 'whatever » plugin:invalid-parser\/foo'/u); + }); - it("should throw an error when an eslint config is not found", () => { - assert.throws(() => { - applyExtends({ - extends: "eslint:foo", - rules: { eqeqeq: 2 } + it("should fall back to default parser when a parser called 'espree' is not found", async () => { + const config = applyExtends({ parser: "espree" }); + + assertConfig(config, { + + // parser: await import.meta.resolve("espree") + parser: require.resolve("espree") }); - }, /Failed to load config "eslint:foo" to extend from./u); - }); + }); - it("should throw an error when a parser in a plugin config is not found", () => { - assert.throws(() => { - applyExtends({ - extends: "plugin:invalid-parser/foo", + it("should throw an error when a plugin config is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "plugin:invalid-config/bar", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "plugin:invalid-config\/bar" to extend from./u); + }); + + it("should throw an error with a message template when a plugin config specifier is missing config name", () => { + try { + applyExtends({ + extends: "plugin:some-plugin", + rules: { eqeqeq: 2 } + }); + } catch (err) { + assert.strictEqual(err.messageTemplate, "plugin-invalid"); + assert.deepStrictEqual(err.messageData, { + configName: "plugin:some-plugin", + importerName: path.join(getPath(), "whatever") + }); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should throw an error with a message template when a plugin referenced for a plugin config is not found", () => { + try { + applyExtends({ + extends: "plugin:nonexistent-plugin/baz", + rules: { eqeqeq: 2 } + }); + } catch (err) { + assert.strictEqual(err.messageTemplate, "plugin-missing"); + assert.deepStrictEqual(err.messageData, { + pluginName: "eslint-plugin-nonexistent-plugin", + resolvePluginsRelativeTo: getPath(), + importerName: "whatever" + }); + return; + } + + assert.fail("Expected to throw an error"); + }); + + it("should throw an error with a message template when a plugin in the plugins list is not found", () => { + try { + applyExtends({ + plugins: ["nonexistent-plugin"] + }); + } catch (err) { + assert.strictEqual(err.messageTemplate, "plugin-missing"); + assert.deepStrictEqual(err.messageData, { + pluginName: "eslint-plugin-nonexistent-plugin", + resolvePluginsRelativeTo: getPath(), + importerName: "whatever" + }); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should apply extensions recursively when specified from package", () => { + const config = applyExtends({ + extends: "one", rules: { eqeqeq: 2 } }); - }, /Failed to load parser 'nonexistent-parser' declared in 'whatever » plugin:invalid-parser\/foo'/u); - }); - it("should fall back to default parser when a parser called 'espree' is not found", async () => { - const config = applyExtends({ parser: "espree" }); + assertConfig(config, { + env: { browser: true, node: true }, + rules: { eqeqeq: [2] } + }); + }); - assertConfig(config, { + it("should apply extensions when specified from a JavaScript file", () => { + const config = applyExtends({ + extends: ".eslintrc.js", + rules: { eqeqeq: 2 } + }, "js/foo.js"); - // parser: await import.meta.resolve("espree") - parser: require.resolve("espree") + assertConfig(config, { + rules: { + semi: [2, "always"], + eqeqeq: [2] + } + }); }); - }); - it("should throw an error when a plugin config is not found", () => { - assert.throws(() => { - applyExtends({ - extends: "plugin:invalid-config/bar", + it("should apply extensions when specified from a YAML file", () => { + const config = applyExtends({ + extends: ".eslintrc.yaml", rules: { eqeqeq: 2 } + }, "yaml/foo.js"); + + assertConfig(config, { + env: { browser: true }, + rules: { + eqeqeq: [2] + } }); - }, /Failed to load config "plugin:invalid-config\/bar" to extend from./u); - }); + }); - it("should throw an error with a message template when a plugin config specifier is missing config name", () => { - try { - applyExtends({ - extends: "plugin:some-plugin", + it("should apply extensions when specified from a JSON file", () => { + const config = applyExtends({ + extends: ".eslintrc.json", rules: { eqeqeq: 2 } + }, "json/foo.js"); + + assertConfig(config, { + rules: { + eqeqeq: [2], + quotes: [2, "double"] + } }); - } catch (err) { - assert.strictEqual(err.messageTemplate, "plugin-invalid"); - assert.deepStrictEqual(err.messageData, { - configName: "plugin:some-plugin", - importerName: path.join(getPath(), "whatever") - }); - return; - } - assert.fail("Expected to throw an error"); - }); + }); - it("should throw an error with a message template when a plugin referenced for a plugin config is not found", () => { - try { - applyExtends({ - extends: "plugin:nonexistent-plugin/baz", + it("should apply extensions when specified from a package.json file in a sibling directory", () => { + const config = applyExtends({ + extends: "../package-json/package.json", rules: { eqeqeq: 2 } + }, "json/foo.js"); + + assertConfig(config, { + env: { es6: true }, + rules: { + eqeqeq: [2] + } }); - } catch (err) { - assert.strictEqual(err.messageTemplate, "plugin-missing"); - assert.deepStrictEqual(err.messageData, { - pluginName: "eslint-plugin-nonexistent-plugin", - resolvePluginsRelativeTo: getPath(), - importerName: "whatever" + }); + }); + + describe("with eslint built-in config callbacks", () => { + let factory; + + beforeEach(async () => { + await prepare(); + factory = new ConfigArrayFactory({ + cwd: getPath(), + getEslintAllConfig, + getEslintRecommendedConfig }); - return; + }); + + afterEach(cleanup); + + /** + * Apply `extends` property. + * @param {Object} configData The config that has `extends` property. + * @param {string} [filePath] The path to the config data. + * @returns {Object} The applied config data. + */ + function applyExtends(configData, filePath = "whatever") { + return factory + .create(configData, { filePath }) + .extractConfig(filePath) + .toCompatibleObjectAsConfigFileContent(); } - assert.fail("Expected to throw an error"); - }); + it("should apply extension 'foo' when specified from root directory config", () => { + const config = applyExtends({ + extends: "foo", + rules: { eqeqeq: 2 } + }); - it("should throw an error with a message template when a plugin in the plugins list is not found", () => { - try { - applyExtends({ - plugins: ["nonexistent-plugin"] + assertConfig(config, { + env: { browser: true }, + rules: { eqeqeq: [2] } }); - } catch (err) { - assert.strictEqual(err.messageTemplate, "plugin-missing"); - assert.deepStrictEqual(err.messageData, { - pluginName: "eslint-plugin-nonexistent-plugin", - resolvePluginsRelativeTo: getPath(), - importerName: "whatever" + }); + + it("should apply all rules when extends config includes 'eslint:all'", () => { + const config = applyExtends({ + extends: "eslint:all" }); - return; - } - assert.fail("Expected to throw an error"); - }); - it("should apply extensions recursively when specified from package", () => { - const config = applyExtends({ - extends: "one", - rules: { eqeqeq: 2 } + assert.strictEqual(config.rules.eqeqeq[0], "error"); + assert.strictEqual(config.rules.curly[0], "error"); }); - assertConfig(config, { - env: { browser: true, node: true }, - rules: { eqeqeq: [2] } + it("should throw an error when extends config module is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "not-exist", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "not-exist" to extend from./u); }); - }); - it("should apply extensions when specified from a JavaScript file", () => { - const config = applyExtends({ - extends: ".eslintrc.js", - rules: { eqeqeq: 2 } - }, "js/foo.js"); + it("should throw an error when an eslint config is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "eslint:foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "eslint:foo" to extend from./u); + }); - assertConfig(config, { - rules: { - semi: [2, "always"], - eqeqeq: [2] - } + it("should throw an error when a parser in a plugin config is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "plugin:invalid-parser/foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load parser 'nonexistent-parser' declared in 'whatever » plugin:invalid-parser\/foo'/u); }); - }); - it("should apply extensions when specified from a YAML file", () => { - const config = applyExtends({ - extends: ".eslintrc.yaml", - rules: { eqeqeq: 2 } - }, "yaml/foo.js"); + it("should fall back to default parser when a parser called 'espree' is not found", async () => { + const config = applyExtends({ parser: "espree" }); - assertConfig(config, { - env: { browser: true }, - rules: { - eqeqeq: [2] - } + assertConfig(config, { + + // parser: await import.meta.resolve("espree") + parser: require.resolve("espree") + }); }); - }); - it("should apply extensions when specified from a JSON file", () => { - const config = applyExtends({ - extends: ".eslintrc.json", - rules: { eqeqeq: 2 } - }, "json/foo.js"); + it("should throw an error when a plugin config is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "plugin:invalid-config/bar", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "plugin:invalid-config\/bar" to extend from./u); + }); - assertConfig(config, { - rules: { - eqeqeq: [2], - quotes: [2, "double"] + it("should throw an error with a message template when a plugin config specifier is missing config name", () => { + try { + applyExtends({ + extends: "plugin:some-plugin", + rules: { eqeqeq: 2 } + }); + } catch (err) { + assert.strictEqual(err.messageTemplate, "plugin-invalid"); + assert.deepStrictEqual(err.messageData, { + configName: "plugin:some-plugin", + importerName: path.join(getPath(), "whatever") + }); + return; } + assert.fail("Expected to throw an error"); }); - }); - it("should apply extensions when specified from a package.json file in a sibling directory", () => { - const config = applyExtends({ - extends: "../package-json/package.json", - rules: { eqeqeq: 2 } - }, "json/foo.js"); + it("should throw an error with a message template when a plugin referenced for a plugin config is not found", () => { + try { + applyExtends({ + extends: "plugin:nonexistent-plugin/baz", + rules: { eqeqeq: 2 } + }); + } catch (err) { + assert.strictEqual(err.messageTemplate, "plugin-missing"); + assert.deepStrictEqual(err.messageData, { + pluginName: "eslint-plugin-nonexistent-plugin", + resolvePluginsRelativeTo: getPath(), + importerName: "whatever" + }); + return; + } - assertConfig(config, { - env: { es6: true }, - rules: { - eqeqeq: [2] + assert.fail("Expected to throw an error"); + }); + + it("should throw an error with a message template when a plugin in the plugins list is not found", () => { + try { + applyExtends({ + plugins: ["nonexistent-plugin"] + }); + } catch (err) { + assert.strictEqual(err.messageTemplate, "plugin-missing"); + assert.deepStrictEqual(err.messageData, { + pluginName: "eslint-plugin-nonexistent-plugin", + resolvePluginsRelativeTo: getPath(), + importerName: "whatever" + }); + return; } + assert.fail("Expected to throw an error"); + }); + + it("should apply extensions recursively when specified from package", () => { + const config = applyExtends({ + extends: "one", + rules: { eqeqeq: 2 } + }); + + assertConfig(config, { + env: { browser: true, node: true }, + rules: { eqeqeq: [2] } + }); + }); + + it("should apply extensions when specified from a JavaScript file", () => { + const config = applyExtends({ + extends: ".eslintrc.js", + rules: { eqeqeq: 2 } + }, "js/foo.js"); + + assertConfig(config, { + rules: { + semi: [2, "always"], + eqeqeq: [2] + } + }); + }); + + it("should apply extensions when specified from a YAML file", () => { + const config = applyExtends({ + extends: ".eslintrc.yaml", + rules: { eqeqeq: 2 } + }, "yaml/foo.js"); + + assertConfig(config, { + env: { browser: true }, + rules: { + eqeqeq: [2] + } + }); + }); + + it("should apply extensions when specified from a JSON file", () => { + const config = applyExtends({ + extends: ".eslintrc.json", + rules: { eqeqeq: 2 } + }, "json/foo.js"); + + assertConfig(config, { + rules: { + eqeqeq: [2], + quotes: [2, "double"] + } + }); + }); + + it("should apply extensions when specified from a package.json file in a sibling directory", () => { + const config = applyExtends({ + extends: "../package-json/package.json", + rules: { eqeqeq: 2 } + }, "json/foo.js"); + + assertConfig(config, { + env: { es6: true }, + rules: { + eqeqeq: [2] + } + }); }); }); }); @@ -1809,7 +2363,7 @@ describe("ConfigArrayFactory", () => { let cleanup; beforeEach(() => { - cleanup = () => {}; + cleanup = () => { }; }); afterEach(() => cleanup()); From 32470f689da0ff525dc70d7f9ad31b1d3ac38730 Mon Sep 17 00:00:00 2001 From: DoZerg Date: Wed, 16 Feb 2022 22:23:49 +0000 Subject: [PATCH 2/2] feat: Avoid dirname for built-in configs. Load eslint:recommended and eslint:all configs via import instead file paths. Fixes: https://github.com/eslint/eslint/issues/15575 --- conf/eslint-all.cjs | 12 - conf/eslint-recommended.cjs | 12 - lib/cascading-config-array-factory.js | 23 +- lib/config-array-factory.js | 50 +- lib/flat-compat.js | 11 +- tests/lib/cascading-config-array-factory.js | 3255 +++++++++++++------ tests/lib/config-array-factory.js | 1352 +++++--- 7 files changed, 3276 insertions(+), 1439 deletions(-) delete mode 100644 conf/eslint-all.cjs delete mode 100644 conf/eslint-recommended.cjs diff --git a/conf/eslint-all.cjs b/conf/eslint-all.cjs deleted file mode 100644 index 859811c8..00000000 --- a/conf/eslint-all.cjs +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @fileoverview Stub eslint:all config - * @author Nicholas C. Zakas - */ - -"use strict"; - -module.exports = { - settings: { - "eslint:all": true - } -}; diff --git a/conf/eslint-recommended.cjs b/conf/eslint-recommended.cjs deleted file mode 100644 index 96300919..00000000 --- a/conf/eslint-recommended.cjs +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @fileoverview Stub eslint:recommended config - * @author Nicholas C. Zakas - */ - -"use strict"; - -module.exports = { - settings: { - "eslint:recommended": true - } -}; diff --git a/lib/cascading-config-array-factory.js b/lib/cascading-config-array-factory.js index 553ca0a9..4b575c2d 100644 --- a/lib/cascading-config-array-factory.js +++ b/lib/cascading-config-array-factory.js @@ -22,13 +22,18 @@ // Requirements //------------------------------------------------------------------------------ +import debugOrig from "debug"; import os from "os"; import path from "path"; + +import { ConfigArrayFactory } from "./config-array-factory.js"; +import { + ConfigArray, + ConfigDependency, + IgnorePattern +} from "./config-array/index.js"; import ConfigValidator from "./shared/config-validator.js"; import { emitDeprecationWarning } from "./shared/deprecation-warnings.js"; -import { ConfigArrayFactory } from "./config-array-factory.js"; -import { ConfigArray, ConfigDependency, IgnorePattern } from "./config-array/index.js"; -import debugOrig from "debug"; const debug = debugOrig("eslintrc:cascading-config-array-factory"); @@ -57,7 +62,9 @@ const debug = debugOrig("eslintrc:cascading-config-array-factory"); * @property {Map} builtInRules The rules that are built in to ESLint. * @property {Object} [resolver=ModuleResolver] The module resolver object. * @property {string} eslintAllPath The path to the definitions for eslint:all. + * @property {Function} getEslintAllConfig Returns the config data for eslint:all. * @property {string} eslintRecommendedPath The path to the definitions for eslint:recommended. + * @property {Function} getEslintRecommendedConfig Returns the config data for eslint:recommended. */ /** @@ -78,7 +85,9 @@ const debug = debugOrig("eslintrc:cascading-config-array-factory"); * @property {Map} builtInRules The rules that are built in to ESLint. * @property {Object} [resolver=ModuleResolver] The module resolver object. * @property {string} eslintAllPath The path to the definitions for eslint:all. + * @property {Function} getEslintAllConfig Returns the config data for eslint:all. * @property {string} eslintRecommendedPath The path to the definitions for eslint:recommended. + * @property {Function} getEslintRecommendedConfig Returns the config data for eslint:recommended. */ /** @type {WeakMap} */ @@ -219,7 +228,9 @@ class CascadingConfigArrayFactory { loadRules, resolver, eslintRecommendedPath, - eslintAllPath + getEslintRecommendedConfig, + eslintAllPath, + getEslintAllConfig } = {}) { const configArrayFactory = new ConfigArrayFactory({ additionalPluginPool, @@ -228,7 +239,9 @@ class CascadingConfigArrayFactory { builtInRules, resolver, eslintRecommendedPath, - eslintAllPath + getEslintRecommendedConfig, + eslintAllPath, + getEslintAllConfig }); internalSlotsMap.set(this, { diff --git a/lib/config-array-factory.js b/lib/config-array-factory.js index b571e2f7..687f0bf2 100644 --- a/lib/config-array-factory.js +++ b/lib/config-array-factory.js @@ -38,22 +38,23 @@ // Requirements //------------------------------------------------------------------------------ +import debugOrig from "debug"; import fs from "fs"; -import path from "path"; import importFresh from "import-fresh"; +import { createRequire } from "module"; +import path from "path"; import stripComments from "strip-json-comments"; -import ConfigValidator from "./shared/config-validator.js"; -import * as naming from "./shared/naming.js"; -import * as ModuleResolver from "./shared/relative-module-resolver.js"; + import { ConfigArray, ConfigDependency, IgnorePattern, OverrideTester } from "./config-array/index.js"; -import debugOrig from "debug"; +import ConfigValidator from "./shared/config-validator.js"; +import * as naming from "./shared/naming.js"; +import * as ModuleResolver from "./shared/relative-module-resolver.js"; -import { createRequire } from "module"; const require = createRequire(import.meta.url); const debug = debugOrig("eslintrc:config-array-factory"); @@ -90,7 +91,9 @@ const configFilenames = [ * @property {Map} builtInRules The rules that are built in to ESLint. * @property {Object} [resolver=ModuleResolver] The module resolver object. * @property {string} eslintAllPath The path to the definitions for eslint:all. + * @property {Function} getEslintAllConfig Returns the config data for eslint:all. * @property {string} eslintRecommendedPath The path to the definitions for eslint:recommended. + * @property {Function} getEslintRecommendedConfig Returns the config data for eslint:recommended. */ /** @@ -101,7 +104,9 @@ const configFilenames = [ * @property {Map} builtInRules The rules that are built in to ESLint. * @property {Object} [resolver=ModuleResolver] The module resolver object. * @property {string} eslintAllPath The path to the definitions for eslint:all. + * @property {Function} getEslintAllConfig Returns the config data for eslint:all. * @property {string} eslintRecommendedPath The path to the definitions for eslint:recommended. + * @property {Function} getEslintRecommendedConfig Returns the config data for eslint:recommended. */ /** @@ -428,7 +433,9 @@ class ConfigArrayFactory { builtInRules, resolver = ModuleResolver, eslintAllPath, - eslintRecommendedPath + getEslintAllConfig, + eslintRecommendedPath, + getEslintRecommendedConfig } = {}) { internalSlotsMap.set(this, { additionalPluginPool, @@ -439,7 +446,9 @@ class ConfigArrayFactory { builtInRules, resolver, eslintAllPath, - eslintRecommendedPath + getEslintAllConfig, + eslintRecommendedPath, + getEslintRecommendedConfig }); } @@ -797,20 +806,35 @@ class ConfigArrayFactory { * @private */ _loadExtendedBuiltInConfig(extendName, ctx) { - const { eslintAllPath, eslintRecommendedPath } = internalSlotsMap.get(this); + const { + eslintAllPath, + getEslintAllConfig, + eslintRecommendedPath, + getEslintRecommendedConfig + } = internalSlotsMap.get(this); if (extendName === "eslint:recommended") { + const name = `${ctx.name} » ${extendName}`; + + if (getEslintRecommendedConfig) { + return this._normalizeConfigData(getEslintRecommendedConfig(), { ...ctx, name, filePath: "" }); + } return this._loadConfigData({ ...ctx, - filePath: eslintRecommendedPath, - name: `${ctx.name} » ${extendName}` + name, + filePath: eslintRecommendedPath }); } if (extendName === "eslint:all") { + const name = `${ctx.name} » ${extendName}`; + + if (getEslintAllConfig) { + return this._normalizeConfigData(getEslintAllConfig(), { ...ctx, name, filePath: "" }); + } return this._loadConfigData({ ...ctx, - filePath: eslintAllPath, - name: `${ctx.name} » ${extendName}` + name, + filePath: eslintAllPath }); } diff --git a/lib/flat-compat.js b/lib/flat-compat.js index 7fa111d3..8df15a53 100644 --- a/lib/flat-compat.js +++ b/lib/flat-compat.js @@ -7,14 +7,11 @@ // Requirements //----------------------------------------------------------------------------- -import path from "path"; -import { fileURLToPath } from "url"; import createDebug from "debug"; +import path from "path"; -import { ConfigArrayFactory } from "./config-array-factory.js"; import environments from "../conf/environments.js"; - -const dirname = path.dirname(fileURLToPath(import.meta.url)); +import { ConfigArrayFactory } from "./config-array-factory.js"; //----------------------------------------------------------------------------- // Helpers @@ -225,8 +222,8 @@ class FlatCompat { this[cafactory] = new ConfigArrayFactory({ cwd: baseDirectory, resolvePluginsRelativeTo, - eslintAllPath: path.resolve(dirname, "../conf/eslint-all.cjs"), - eslintRecommendedPath: path.resolve(dirname, "../conf/eslint-recommended.cjs") + getEslintAllConfig: () => ({ settings: { "eslint:all": true } }), + getEslintRecommendedConfig: () => ({ settings: { "eslint:recommended": true } }) }); } diff --git a/tests/lib/cascading-config-array-factory.js b/tests/lib/cascading-config-array-factory.js index 64b70395..7250f9a8 100644 --- a/tests/lib/cascading-config-array-factory.js +++ b/tests/lib/cascading-config-array-factory.js @@ -7,18 +7,21 @@ // Requirements //----------------------------------------------------------------------------- +import { assert } from "chai"; import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; +import { createRequire } from "module"; import os from "os"; -import { assert } from "chai"; +import path from "path"; import sh from "shelljs"; import sinon from "sinon"; import systemTempDir from "temp-dir"; +import { fileURLToPath } from "url"; import { Legacy } from "../../lib/index.js"; import { createCustomTeardown } from "../_utils/index.js"; +const require = createRequire(import.meta.url); + const dirname = path.dirname(fileURLToPath(import.meta.url)); const { @@ -41,6 +44,22 @@ const cwdIgnorePatterns = new ConfigArrayFactory() const eslintAllPath = path.resolve(dirname, "../fixtures/eslint-all.cjs"); const eslintRecommendedPath = path.resolve(dirname, "../fixtures/eslint-recommended.cjs"); +/** + * Return config data for built-in eslint:all. + * @returns {ConfigData} Config data + */ +function getEslintAllConfig() { + return require("../fixtures/eslint-all.cjs"); +} + +/** + * Return config data for built-in eslint:recommended. + * @returns {ConfigData} Config data + */ +function getEslintRecommendedConfig() { + return require("../fixtures/eslint-recommended.cjs"); +} + //----------------------------------------------------------------------------- // Tests //----------------------------------------------------------------------------- @@ -528,7 +547,6 @@ describe("CascadingConfigArrayFactory", () => { // This group moved from 'tests/lib/config.js' when refactoring to keep the cumulated test cases. describe("with 'tests/fixtures/config-hierarchy' files", () => { - let fixtureDir; // hack to avoid needing to hand-rewrite file-structure.json const DIRECTORY_CONFIG_HIERARCHY = (() => { @@ -561,16 +579,6 @@ describe("CascadingConfigArrayFactory", () => { return flattened; })(); - /** - * Returns the path inside of the fixture directory. - * @param {...string} args file path segments. - * @returns {string} The path inside the fixture directory. - * @private - */ - function getFixturePath(...args) { - return path.join(fixtureDir, "config-hierarchy", ...args); - } - /** * Mocks the current user's home path * @param {string} fakeUserHomePath fake user's home path @@ -624,1217 +632,2414 @@ describe("CascadingConfigArrayFactory", () => { .toCompatibleObjectAsConfigFileContent(); } - // copy into clean area so as not to get "infected" by this project's .eslintrc files - before(function() { + describe("with eslint built-in config paths", () => { + let fixtureDir; - /* - * GitHub Actions Windows and macOS runners occasionally exhibit - * extremely slow filesystem operations, during which copying fixtures - * exceeds the default test timeout, so raise it just for this hook. - * Mocha uses `this` to set timeouts on an individual hook level. + /** + * Returns the path inside of the fixture directory. + * @param {...string} args file path segments. + * @returns {string} The path inside the fixture directory. + * @private */ - this.timeout(60 * 1000); // eslint-disable-line no-invalid-this - - fixtureDir = `${systemTempDir}/eslint/fixtures`; - sh.mkdir("-p", fixtureDir); - sh.cp("-r", "./tests/fixtures/config-hierarchy", fixtureDir); - sh.cp("-r", "./tests/fixtures/rules", fixtureDir); - }); + function getFixturePath(...args) { + return path.join(fixtureDir, "config-hierarchy", ...args); + } - afterEach(() => { - sinon.verifyAndRestore(); - }); + // copy into clean area so as not to get "infected" by this project's .eslintrc files + before(function() { - after(() => { - sh.rm("-r", fixtureDir); - }); + /* + * GitHub Actions Windows and macOS runners occasionally exhibit + * extremely slow filesystem operations, during which copying fixtures + * exceeds the default test timeout, so raise it just for this hook. + * Mocha uses `this` to set timeouts on an individual hook level. + */ + this.timeout(60 * 1000); // eslint-disable-line no-invalid-this - it("should create config object when using baseConfig with extends", () => { - const customBaseConfig = { - extends: path.resolve(dirname, "../fixtures/config-extends/array/.eslintrc") - }; - const factory = new CascadingConfigArrayFactory({ - cwd: fixtureDir, - baseConfig: customBaseConfig, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + fixtureDir = `${systemTempDir}/eslint/fixtures`; + sh.mkdir("-p", fixtureDir); + sh.cp("-r", "./tests/fixtures/config-hierarchy", fixtureDir); + sh.cp("-r", "./tests/fixtures/rules", fixtureDir); }); - const config = getConfig(factory); - assert.deepStrictEqual(config.env, { - browser: false, - es6: true, - node: true + afterEach(() => { + sinon.verifyAndRestore(); }); - assert.deepStrictEqual(config.rules, { - "no-empty": [1], - "comma-dangle": [2], - "no-console": [2] + + after(() => { + sh.rm("-r", fixtureDir); }); - }); - // TODO: Tests should not rely on project files!!! - it("should return the project config when called in current working directory", () => { - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath + it("should create config object when using baseConfig with extends", () => { + const customBaseConfig = { + extends: path.resolve(dirname, "../fixtures/config-extends/array/.eslintrc") + }; + const factory = new CascadingConfigArrayFactory({ + cwd: fixtureDir, + baseConfig: customBaseConfig, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const config = getConfig(factory); + + assert.deepStrictEqual(config.env, { + browser: false, + es6: true, + node: true + }); + assert.deepStrictEqual(config.rules, { + "no-empty": [1], + "comma-dangle": [2], + "no-console": [2] + }); }); - const actual = getConfig(factory); - assert.strictEqual(actual.rules.strict[1], "global"); - }); + // TODO: Tests should not rely on project files!!! + it("should return the project config when called in current working directory", () => { + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); + const actual = getConfig(factory); - it("should not retain configs from previous directories when called multiple times", () => { - const firstpath = path.resolve(dirname, "../fixtures/configurations/single-quotes/subdir/.eslintrc"); - const secondpath = path.resolve(dirname, "../fixtures/configurations/single-quotes/.eslintrc"); - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath + assert.strictEqual(actual.rules.strict[1], "global"); }); - let config; - config = getConfig(factory, firstpath); - assert.deepStrictEqual(config.rules["no-new"], [0]); - config = getConfig(factory, secondpath); - assert.deepStrictEqual(config.rules["no-new"], [1]); - }); + it("should not retain configs from previous directories when called multiple times", () => { + const firstpath = path.resolve(dirname, "../fixtures/configurations/single-quotes/subdir/.eslintrc"); + const secondpath = path.resolve(dirname, "../fixtures/configurations/single-quotes/.eslintrc"); + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); + let config; - it("should throw error when a configuration file doesn't exist", () => { - const configPath = path.resolve(dirname, "../fixtures/configurations/.eslintrc"); - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath + config = getConfig(factory, firstpath); + assert.deepStrictEqual(config.rules["no-new"], [0]); + config = getConfig(factory, secondpath); + assert.deepStrictEqual(config.rules["no-new"], [1]); }); - sinon.stub(fs, "readFileSync").throws(new Error()); + it("should throw error when a configuration file doesn't exist", () => { + const configPath = path.resolve(dirname, "../fixtures/configurations/.eslintrc"); + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); - assert.throws(() => { - getConfig(factory, configPath); - }, "Cannot read config file"); + sinon.stub(fs, "readFileSync").throws(new Error()); - }); + assert.throws(() => { + getConfig(factory, configPath); + }, "Cannot read config file"); - it("should throw error when a configuration file is not require-able", () => { - const configPath = ".eslintrc"; - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath }); - sinon.stub(fs, "readFileSync").throws(new Error()); + it("should throw error when a configuration file is not require-able", () => { + const configPath = ".eslintrc"; + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); - assert.throws(() => { - getConfig(factory, configPath); - }, "Cannot read config file"); + sinon.stub(fs, "readFileSync").throws(new Error()); - }); + assert.throws(() => { + getConfig(factory, configPath); + }, "Cannot read config file"); - it("should cache config when the same directory is passed twice", () => { - const configPath = path.resolve(dirname, "../fixtures/configurations/single-quotes/.eslintrc"); - const configArrayFactory = new ConfigArrayFactory(); - const factory = new CascadingConfigArrayFactory({ - configArrayFactory, - eslintAllPath, - eslintRecommendedPath }); - sinon.spy(configArrayFactory, "loadInDirectory"); + it("should cache config when the same directory is passed twice", () => { + const configPath = path.resolve(dirname, "../fixtures/configurations/single-quotes/.eslintrc"); + const configArrayFactory = new ConfigArrayFactory(); + const factory = new CascadingConfigArrayFactory({ + configArrayFactory, + eslintAllPath, + eslintRecommendedPath + }); - // If cached this should be called only once - getConfig(factory, configPath); - const callcount = configArrayFactory.loadInDirectory.callcount; + sinon.spy(configArrayFactory, "loadInDirectory"); - getConfig(factory, configPath); + // If cached this should be called only once + getConfig(factory, configPath); + const callcount = configArrayFactory.loadInDirectory.callcount; - assert.strictEqual(configArrayFactory.loadInDirectory.callcount, callcount); - }); + getConfig(factory, configPath); - // make sure JS-style comments don't throw an error - it("should load the config file when there are JS-style comments in the text", () => { - const specificConfigPath = path.resolve(dirname, "../fixtures/configurations/comments.json"); - const factory = new CascadingConfigArrayFactory({ - specificConfigPath, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + assert.strictEqual(configArrayFactory.loadInDirectory.callcount, callcount); }); - const config = getConfig(factory); - const { semi, strict } = config.rules; - assert.deepStrictEqual(semi, [1]); - assert.deepStrictEqual(strict, [0]); - }); + // make sure JS-style comments don't throw an error + it("should load the config file when there are JS-style comments in the text", () => { + const specificConfigPath = path.resolve(dirname, "../fixtures/configurations/comments.json"); + const factory = new CascadingConfigArrayFactory({ + specificConfigPath, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const config = getConfig(factory); + const { semi, strict } = config.rules; - // make sure YAML files work correctly - it("should load the config file when a YAML file is used", () => { - const specificConfigPath = path.resolve(dirname, "../fixtures/configurations/env-browser.yaml"); - const factory = new CascadingConfigArrayFactory({ - specificConfigPath, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + assert.deepStrictEqual(semi, [1]); + assert.deepStrictEqual(strict, [0]); }); - const config = getConfig(factory); - const { "no-alert": noAlert, "no-undef": noUndef } = config.rules; - assert.deepStrictEqual(noAlert, [0]); - assert.deepStrictEqual(noUndef, [2]); - }); + // make sure YAML files work correctly + it("should load the config file when a YAML file is used", () => { + const specificConfigPath = path.resolve(dirname, "../fixtures/configurations/env-browser.yaml"); + const factory = new CascadingConfigArrayFactory({ + specificConfigPath, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const config = getConfig(factory); + const { "no-alert": noAlert, "no-undef": noUndef } = config.rules; - it("should contain the correct value for parser when a custom parser is specified", () => { - const configPath = path.resolve(dirname, "../fixtures/configurations/parser/.eslintrc.json"); - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath + assert.deepStrictEqual(noAlert, [0]); + assert.deepStrictEqual(noUndef, [2]); }); - const config = getConfig(factory, configPath); - assert.strictEqual(config.parser, path.resolve(path.dirname(configPath), "./custom.cjs")); - }); + it("should contain the correct value for parser when a custom parser is specified", () => { + const configPath = path.resolve(dirname, "../fixtures/configurations/parser/.eslintrc.json"); + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); + const config = getConfig(factory, configPath); - /* - * Configuration hierarchy --------------------------------------------- - * https://github.com/eslint/eslint/issues/3915 - */ - it("should correctly merge environment settings", () => { - const factory = new CascadingConfigArrayFactory({ - useEslintrc: true, - eslintAllPath, - eslintRecommendedPath - }); - const file = getFixturePath("envs", "sub", "foo.js"); - const expected = { - rules: {}, - env: { - browser: true, - node: false - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); + assert.strictEqual(config.parser, path.resolve(path.dirname(configPath), "./custom.cjs")); + }); - assertConfigsEqual(actual, expected); - }); + /* + * Configuration hierarchy --------------------------------------------- + * https://github.com/eslint/eslint/issues/3915 + */ + it("should correctly merge environment settings", () => { + const factory = new CascadingConfigArrayFactory({ + useEslintrc: true, + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("envs", "sub", "foo.js"); + const expected = { + rules: {}, + env: { + browser: true, + node: false + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - // Default configuration - blank - it("should return a blank config when using no .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath - }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); - const expected = { - rules: {}, - globals: {}, - env: {}, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); + assertConfigsEqual(actual, expected); + }); - assertConfigsEqual(actual, expected); - }); + // Default configuration - blank + it("should return a blank config when using no .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + rules: {}, + globals: {}, + env: {}, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - it("should return a blank config when baseConfig is set to false and no .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - baseConfig: false, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath - }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); - const expected = { - rules: {}, - globals: {}, - env: {}, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); + assertConfigsEqual(actual, expected); + }); - assertConfigsEqual(actual, expected); - }); + it("should return a blank config when baseConfig is set to false and no .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + baseConfig: false, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + rules: {}, + globals: {}, + env: {}, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - // No default configuration - it("should return an empty config when not using .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + assertConfigsEqual(actual, expected); }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); - const actual = getConfig(factory, file); - assertConfigsEqual(actual, { ignorePatterns: cwdIgnorePatterns }); - }); + // No default configuration + it("should return an empty config when not using .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, { ignorePatterns: cwdIgnorePatterns }); + }); - it("should return a modified config when baseConfig is set to an object and no .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - baseConfig: { + it("should return a modified config when baseConfig is set to an object and no .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + baseConfig: { + env: { + node: true + }, + rules: { + quotes: [2, "single"] + } + }, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { env: { node: true }, rules: { quotes: [2, "single"] - } - }, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath - }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - rules: { - quotes: [2, "single"] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); - }); + assertConfigsEqual(actual, expected); + }); - it("should return a modified config without plugin rules enabled when baseConfig is set to an object with plugin and no .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - baseConfig: { + it("should return a modified config without plugin rules enabled when baseConfig is set to an object with plugin and no .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + baseConfig: { + env: { + node: true + }, + rules: { + quotes: [2, "single"] + }, + plugins: ["example-with-rules-config"] + }, + cwd: getFixturePath("plugins"), + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"); + const expected = { env: { node: true }, + plugins: ["example-with-rules-config"], rules: { quotes: [2, "single"] - }, - plugins: ["example-with-rules-config"] - }, - cwd: getFixturePath("plugins"), - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath - }); - const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - plugins: ["example-with-rules-config"], - rules: { - quotes: [2, "single"] - } - }; - const actual = getConfig(factory, file); - - assertConfigsEqual(actual, expected); - }); - - // Project configuration - second level .eslintrc - it("should merge configs when local .eslintrc overrides parent .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath - }); - const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - rules: { - "no-console": [1], - quotes: [2, "single"] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); - - assertConfigsEqual(actual, expected); - }); + } + }; + const actual = getConfig(factory, file); - // Project configuration - third level .eslintrc - it("should merge configs when local .eslintrc overrides parent and grandparent .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath + assertConfigsEqual(actual, expected); }); - const file = getFixturePath("broken", "subbroken", "subsubbroken", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - rules: { - "no-console": [0], - quotes: [1, "double"] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); - }); + // Project configuration - second level .eslintrc + it("should merge configs when local .eslintrc overrides parent .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + "no-console": [1], + quotes: [2, "single"] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - // Project configuration - root set in second level .eslintrc - it("should not return or traverse configurations in parents of config with root:true", () => { - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath + assertConfigsEqual(actual, expected); }); - const file = getFixturePath("root-true", "parent", "root", "wrong-semi.js"); - const expected = { - rules: { - semi: [2, "never"] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); - }); + // Project configuration - third level .eslintrc + it("should merge configs when local .eslintrc overrides parent and grandparent .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "subbroken", "subsubbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + "no-console": [0], + quotes: [1, "double"] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - // Project configuration - root set in second level .eslintrc - it("should return project config when called with a relative path from a subdir", () => { - const factory = new CascadingConfigArrayFactory({ - cwd: getFixturePath("root-true", "parent", "root", "subdir"), - eslintRecommendedPath, - eslintAllPath + assertConfigsEqual(actual, expected); }); - const dir = "."; - const expected = { - rules: { - semi: [2, "never"] - } - }; - const actual = getConfig(factory, dir); - assertConfigsEqual(actual, expected); - }); + // Project configuration - root set in second level .eslintrc + it("should not return or traverse configurations in parents of config with root:true", () => { + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("root-true", "parent", "root", "wrong-semi.js"); + const expected = { + rules: { + semi: [2, "never"] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - // Command line configuration - --config with first level .eslintrc - it("should merge command line config when config file adds to local .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: getFixturePath("broken", "add-conf.yaml"), - eslintAllPath, - eslintRecommendedPath + assertConfigsEqual(actual, expected); }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - rules: { - quotes: [2, "double"], - semi: [1, "never"] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); - }); + // Project configuration - root set in second level .eslintrc + it("should return project config when called with a relative path from a subdir", () => { + const factory = new CascadingConfigArrayFactory({ + cwd: getFixturePath("root-true", "parent", "root", "subdir"), + eslintAllPath, + eslintRecommendedPath + }); + const dir = "."; + const expected = { + rules: { + semi: [2, "never"] + } + }; + const actual = getConfig(factory, dir); - // Command line configuration - --config with first level .eslintrc - it("should merge command line config when config file overrides local .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: getFixturePath("broken", "override-conf.yaml"), - eslintAllPath, - eslintRecommendedPath + assertConfigsEqual(actual, expected); }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - rules: { - quotes: [0, "double"] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); - }); + // Command line configuration - --config with first level .eslintrc + it("should merge command line config when config file adds to local .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: getFixturePath("broken", "add-conf.yaml"), + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [2, "double"], + semi: [1, "never"] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - // Command line configuration - --config with second level .eslintrc - it("should merge command line config when config file adds to local and parent .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: getFixturePath("broken", "add-conf.yaml"), - eslintAllPath, - eslintRecommendedPath + assertConfigsEqual(actual, expected); }); - const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - rules: { - quotes: [2, "single"], - "no-console": [1], - semi: [1, "never"] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); - }); + // Command line configuration - --config with first level .eslintrc + it("should merge command line config when config file overrides local .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: getFixturePath("broken", "override-conf.yaml"), + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [0, "double"] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - // Command line configuration - --config with second level .eslintrc - it("should merge command line config when config file overrides local and parent .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: getFixturePath("broken", "override-conf.yaml"), - eslintAllPath, - eslintRecommendedPath + assertConfigsEqual(actual, expected); }); - const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - rules: { - quotes: [0, "single"], - "no-console": [1] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); - }); + // Command line configuration - --config with second level .eslintrc + it("should merge command line config when config file adds to local and parent .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: getFixturePath("broken", "add-conf.yaml"), + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [2, "single"], + "no-console": [1], + semi: [1, "never"] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); - // Command line configuration - --rule with --config and first level .eslintrc - it("should merge command line config and rule when rule and config file overrides local .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - cliConfig: { + // Command line configuration - --config with second level .eslintrc + it("should merge command line config when config file overrides local and parent .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: getFixturePath("broken", "override-conf.yaml"), + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [0, "single"], + "no-console": [1] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + // Command line configuration - --rule with --config and first level .eslintrc + it("should merge command line config and rule when rule and config file overrides local .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + cliConfig: { + rules: { + quotes: [1, "double"] + } + }, + specificConfigPath: getFixturePath("broken", "override-conf.yaml"), + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, rules: { quotes: [1, "double"] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + // Command line configuration - --plugin + it("should merge command line plugin with local .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + cliConfig: { + plugins: ["another-plugin"] + }, + cwd: getFixturePath("plugins"), + resolvePluginsRelativeTo: getFixturePath("plugins"), + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + plugins: [ + "example", + "another-plugin" + ], + rules: { + quotes: [2, "double"] } - }, - specificConfigPath: getFixturePath("broken", "override-conf.yaml"), - eslintAllPath, - eslintRecommendedPath - }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - rules: { - quotes: [1, "double"] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); + }; + const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); - }); + assertConfigsEqual(actual, expected); + }); - // Command line configuration - --plugin - it("should merge command line plugin with local .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - cliConfig: { - plugins: ["another-plugin"] - }, - cwd: getFixturePath("plugins"), - resolvePluginsRelativeTo: getFixturePath("plugins"), - eslintAllPath, - eslintRecommendedPath - }); - const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - plugins: [ - "example", - "another-plugin" - ], - rules: { - quotes: [2, "double"] + + it("should merge multiple different config file formats", () => { + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); + const file = getFixturePath("fileexts/subdir/subsubdir/foo.js"); + const expected = { + env: { + browser: true + }, + rules: { + semi: [2, "always"], + eqeqeq: [2] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + + it("should load user config globals", () => { + const configPath = path.resolve(dirname, "../fixtures/globals/conf.yaml"); + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: configPath, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const expected = { + globals: { + foo: true + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, configPath); + + assertConfigsEqual(actual, expected); + }); + + it("should not load disabled environments", () => { + const configPath = path.resolve(dirname, "../fixtures/environments/disable.yaml"); + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: configPath, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const config = getConfig(factory, configPath); + + assert.isUndefined(config.globals.window); + }); + + it("should gracefully handle empty files", () => { + const configPath = path.resolve(dirname, "../fixtures/configurations/env-node.json"); + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: configPath, + eslintAllPath, + eslintRecommendedPath + }); + + getConfig(factory, path.resolve(dirname, "../fixtures/configurations/empty/empty.json")); + }); + + // Meaningful stack-traces + it("should include references to where an `extends` configuration was loaded from", () => { + const configPath = path.resolve(dirname, "../fixtures/config-extends/error.json"); + + assert.throws(() => { + const factory = new CascadingConfigArrayFactory({ + useEslintrc: false, + specificConfigPath: configPath, + eslintAllPath, + eslintRecommendedPath + }); + + getConfig(factory, configPath); + }, /Referenced from:.*?error\.json/u); + }); + + // Keep order with the last array element taking highest precedence + it("should make the last element in an array take the highest precedence", () => { + const configPath = path.resolve(dirname, "../fixtures/config-extends/array/.eslintrc"); + const factory = new CascadingConfigArrayFactory({ + useEslintrc: false, + specificConfigPath: configPath, + eslintAllPath, + eslintRecommendedPath + }); + const expected = { + rules: { "no-empty": [1], "comma-dangle": [2], "no-console": [2] }, + env: { browser: false, node: true, es6: true }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, configPath); + + assertConfigsEqual(actual, expected); + }); + + describe("with env in a child configuration file", () => { + it("should not overwrite parserOptions of the parent with env of the child", () => { + const factory = new CascadingConfigArrayFactory({ + eslintAllPath, + eslintRecommendedPath + }); + const targetPath = getFixturePath("overwrite-ecmaFeatures", "child", "foo.js"); + const expected = { + rules: {}, + env: { commonjs: true }, + parserOptions: { ecmaFeatures: { globalReturn: false } }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + }); + + describe("personal config file within home directory", () => { + + const root = path.join(systemTempDir, "eslint/cli-engine/cascading-config-array-factory/personal-config"); + + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root, + files: { + ...DIRECTORY_CONFIG_HIERARCHY + } + }); + + before(prepare); + after(cleanup); + + /** + * Returns the path inside of the fixture directory. + * @param {...string} args file path segments. + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFakeFixturePath(...args) { + return path.join(getPath(), "eslint", "fixtures", "config-hierarchy", ...args); } - }; - const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); + it("should load the personal config if no local config was found", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "home-folder"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + eslintAllPath, + eslintRecommendedPath + }); + + mockOsHomedir(homePath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + "home-folder-rule": [2] + } + }; + + assertConfigsEqual(actual, expected); + }); + + it("should ignore the personal config if a local config was found", () => { + const projectPath = getFakeFixturePath("personal-config", "home-folder", "project"); + const homePath = getFakeFixturePath("personal-config", "home-folder"); + const filePath = getFakeFixturePath("personal-config", "home-folder", "project", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + eslintAllPath, + eslintRecommendedPath + }); + + mockOsHomedir(homePath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + "project-level-rule": [2] + } + }; + + assertConfigsEqual(actual, expected); + }); + + it("should ignore the personal config if config is passed through cli", () => { + const configPath = getFakeFixturePath("quotes-error.json"); + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "home-folder"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + specificConfigPath: configPath, + eslintAllPath, + eslintRecommendedPath + }); + + mockOsHomedir(homePath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + quotes: [2, "double"] + } + }; + + assertConfigsEqual(actual, expected); + }); + + it("should still load the project config if the current working directory is the same as the home folder", () => { + const projectPath = getFakeFixturePath("personal-config", "project-with-config"); + const filePath = getFakeFixturePath("personal-config", "project-with-config", "subfolder", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + eslintAllPath, + eslintRecommendedPath + }); + + mockOsHomedir(projectPath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + "project-level-rule": [2], + "subfolder-level-rule": [2] + } + }; + + assertConfigsEqual(actual, expected); + }); + }); + + describe("when no local or personal config is found", () => { + + const root = path.join(systemTempDir, "eslint/cli-engine/cascading-config-array-factory/personal-config"); + + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root, + files: { + ...DIRECTORY_CONFIG_HIERARCHY + } + }); + + before(prepare); + after(cleanup); + + /** + * Returns the path inside of the fixture directory. + * @param {...string} args file path segments. + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFakeFixturePath(...args) { + return path.join(getPath(), "eslint", "fixtures", "config-hierarchy", ...args); + } + + it("should throw an error if no local config and no personal config was found", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + eslintAllPath, + eslintRecommendedPath + }); + + mockOsHomedir(homePath); + + assert.throws(() => { + getConfig(factory, filePath); + }, "No ESLint configuration found"); + }); + + it("should throw an error if no local config was found and ~/package.json contains no eslintConfig section", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "home-folder-with-packagejson"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + eslintAllPath, + eslintRecommendedPath + }); + + mockOsHomedir(homePath); + + assert.throws(() => { + getConfig(factory, filePath); + }, "No ESLint configuration found"); + }); + + it("should not throw an error if no local config and no personal config was found but useEslintrc is false", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + + mockOsHomedir(homePath); + + getConfig(factory, filePath); + }); + + it("should not throw an error if no local config and no personal config was found but rules are specified", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cliConfig: { + rules: { quotes: [2, "single"] } + }, + cwd: projectPath, + eslintAllPath, + eslintRecommendedPath + }); + + mockOsHomedir(homePath); + + getConfig(factory, filePath); + }); + + it("should not throw an error if no local config and no personal config was found but baseConfig is specified", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + baseConfig: {}, + cwd: projectPath, + eslintAllPath, + eslintRecommendedPath + }); + + mockOsHomedir(homePath); + + getConfig(factory, filePath); + }); + }); + + describe("with overrides", () => { + + const root = path.join(systemTempDir, "eslint/cli-engine/cascading-config-array-factory/personal-config"); + + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root, + files: { + ...DIRECTORY_CONFIG_HIERARCHY + } + }); + + before(prepare); + after(cleanup); + + /** + * Returns the path inside of the fixture directory. + * @param {...string} pathSegments One or more path segments, in order of depth, shallowest first + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFakeFixturePath(...pathSegments) { + return path.join(getPath(), "eslint", "fixtures", "config-hierarchy", ...pathSegments); + } + + it("should merge override config when the pattern matches the file name", () => { + const factory = new CascadingConfigArrayFactory({ + cwd: getPath(), + eslintAllPath, + eslintRecommendedPath + }); + const targetPath = getFakeFixturePath("overrides", "foo.js"); + const expected = { + rules: { + quotes: [2, "single"], + "no-else-return": [0], + "no-unused-vars": [1], + semi: [1, "never"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should merge override config when the pattern matches the file path relative to the config file", () => { + const factory = new CascadingConfigArrayFactory({ + cwd: getPath(), + eslintAllPath, + eslintRecommendedPath + }); + const targetPath = getFakeFixturePath("overrides", "child", "child-one.js"); + const expected = { + rules: { + curly: ["error", "multi", "consistent"], + "no-else-return": [0], + "no-unused-vars": [1], + quotes: [2, "double"], + semi: [1, "never"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should not merge override config when the pattern matches the absolute file path", () => { + const resolvedPath = path.resolve(dirname, "../fixtures/config-hierarchy/overrides/bar.cjs"); + + assert.throws(() => new CascadingConfigArrayFactory({ + cwd: getPath(), + baseConfig: { + overrides: [{ + files: resolvedPath, + rules: { + quotes: [1, "double"] + } + }] + }, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }), /Invalid override pattern/u); + }); + + it("should not merge override config when the pattern traverses up the directory tree", () => { + const parentPath = "overrides/../**/*.js"; + + assert.throws(() => new CascadingConfigArrayFactory({ + baseConfig: { + overrides: [{ + files: parentPath, + rules: { + quotes: [1, "single"] + } + }] + }, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }), /Invalid override pattern/u); + }); + + it("should merge all local configs (override and non-override) before non-local configs", () => { + const factory = new CascadingConfigArrayFactory({ + cwd: getPath(), + eslintAllPath, + eslintRecommendedPath + }); + const targetPath = getFakeFixturePath("overrides", "two", "child-two.js"); + const expected = { + rules: { + "no-console": [0], + "no-else-return": [0], + "no-unused-vars": [2], + quotes: [2, "double"], + semi: [2, "never"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides in parent .eslintrc over non-override rules in child .eslintrc", () => { + const targetPath = getFakeFixturePath("overrides", "three", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [ + { + files: "three/**/*.js", + rules: { + "semi-style": [2, "last"] + } + } + ] + }, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const expected = { + rules: { + "semi-style": [2, "last"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides if all glob patterns match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: ["one/**/*", "*.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const expected = { + rules: { + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides even if some glob patterns do not match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: ["one/**/*", "*two.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const expected = { + rules: { + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should not apply overrides if any excluded glob patterns match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: "one/**/*", + excludedFiles: ["two/**/*", "*one.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const expected = { + rules: {} + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides if all excluded glob patterns fail to match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: "one/**/*", + excludedFiles: ["two/**/*", "*two.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const expected = { + rules: { + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should cascade", () => { + const targetPath = getFakeFixturePath("overrides", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [ + { + files: "foo.js", + rules: { + semi: [2, "never"], + quotes: [2, "single"] + } + }, + { + files: "foo.js", + rules: { + semi: [2, "never"], + quotes: [2, "double"] + } + } + ] + }, + useEslintrc: false, + eslintAllPath, + eslintRecommendedPath + }); + const expected = { + rules: { + semi: [2, "never"], + quotes: [2, "double"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + }); + + describe("deprecation warnings", () => { + const cwd = path.resolve(dirname, "../fixtures/config-file/"); + let warning = null; + + /** + * Store a reported warning object if that code starts with `ESLINT_`. + * @param {{code:string, message:string}} w The warning object to store. + * @returns {void} + */ + function onWarning(w) { + if (w.code.startsWith("ESLINT_")) { + warning = w; + } + } + + /** @type {CascadingConfigArrayFactory} */ + let factory; + + beforeEach(() => { + factory = new CascadingConfigArrayFactory({ + cwd, + eslintAllPath, + eslintRecommendedPath + }); + warning = null; + process.on("warning", onWarning); + }); + afterEach(() => { + process.removeListener("warning", onWarning); + }); + + it("should emit a deprecation warning if 'ecmaFeatures' is given.", async () => { + getConfig(factory, "ecma-features/test.js"); + + // Wait for "warning" event. + await nextTick(); + + assert.notStrictEqual(warning, null); + assert.strictEqual( + warning.message, + `The 'ecmaFeatures' config file property is deprecated and has no effect. (found in "ecma-features${path.sep}.eslintrc.yml")` + ); + }); + }); }); + describe("with eslint built-in config callbacks", () => { + let fixtureDir; - it("should merge multiple different config file formats", () => { - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath + /** + * Returns the path inside of the fixture directory. + * @param {...string} args file path segments. + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFixturePath(...args) { + return path.join(fixtureDir, "config-hierarchy", ...args); + } + + // copy into clean area so as not to get "infected" by this project's .eslintrc files + before(function() { + + /* + * GitHub Actions Windows and macOS runners occasionally exhibit + * extremely slow filesystem operations, during which copying fixtures + * exceeds the default test timeout, so raise it just for this hook. + * Mocha uses `this` to set timeouts on an individual hook level. + */ + this.timeout(60 * 1000); // eslint-disable-line no-invalid-this + + fixtureDir = `${systemTempDir}/eslint/fixtures`; + sh.mkdir("-p", fixtureDir); + sh.cp("-r", "./tests/fixtures/config-hierarchy", fixtureDir); + sh.cp("-r", "./tests/fixtures/rules", fixtureDir); }); - const file = getFixturePath("fileexts/subdir/subsubdir/foo.js"); - const expected = { - env: { - browser: true - }, - rules: { - semi: [2, "always"], - eqeqeq: [2] - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); - }); + afterEach(() => { + sinon.verifyAndRestore(); + }); + + after(() => { + sh.rm("-r", fixtureDir); + }); + + it("should create config object when using baseConfig with extends", () => { + const customBaseConfig = { + extends: path.resolve(dirname, "../fixtures/config-extends/array/.eslintrc") + }; + const factory = new CascadingConfigArrayFactory({ + cwd: fixtureDir, + baseConfig: customBaseConfig, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + const config = getConfig(factory); + + assert.deepStrictEqual(config.env, { + browser: false, + es6: true, + node: true + }); + assert.deepStrictEqual(config.rules, { + "no-empty": [1], + "comma-dangle": [2], + "no-console": [2] + }); + }); + + // TODO: Tests should not rely on project files!!! + it("should return the project config when called in current working directory", () => { + const factory = new CascadingConfigArrayFactory({ + getEslintAllConfig, + getEslintRecommendedConfig + }); + const actual = getConfig(factory); + + assert.strictEqual(actual.rules.strict[1], "global"); + }); + + it("should not retain configs from previous directories when called multiple times", () => { + const firstpath = path.resolve(dirname, "../fixtures/configurations/single-quotes/subdir/.eslintrc"); + const secondpath = path.resolve(dirname, "../fixtures/configurations/single-quotes/.eslintrc"); + const factory = new CascadingConfigArrayFactory({ + getEslintAllConfig, + getEslintRecommendedConfig + }); + let config; + + config = getConfig(factory, firstpath); + assert.deepStrictEqual(config.rules["no-new"], [0]); + config = getConfig(factory, secondpath); + assert.deepStrictEqual(config.rules["no-new"], [1]); + }); + + it("should throw error when a configuration file doesn't exist", () => { + const configPath = path.resolve(dirname, "../fixtures/configurations/.eslintrc"); + const factory = new CascadingConfigArrayFactory({ + getEslintAllConfig, + getEslintRecommendedConfig + }); + + sinon.stub(fs, "readFileSync").throws(new Error()); + + assert.throws(() => { + getConfig(factory, configPath); + }, "Cannot read config file"); + + }); + + it("should throw error when a configuration file is not require-able", () => { + const configPath = ".eslintrc"; + const factory = new CascadingConfigArrayFactory({ + getEslintAllConfig, + getEslintRecommendedConfig + }); + + sinon.stub(fs, "readFileSync").throws(new Error()); + assert.throws(() => { + getConfig(factory, configPath); + }, "Cannot read config file"); - it("should load user config globals", () => { - const configPath = path.resolve(dirname, "../fixtures/globals/conf.yaml"); - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: configPath, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath }); - const expected = { - globals: { - foo: true - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, configPath); - assertConfigsEqual(actual, expected); - }); + it("should cache config when the same directory is passed twice", () => { + const configPath = path.resolve(dirname, "../fixtures/configurations/single-quotes/.eslintrc"); + const configArrayFactory = new ConfigArrayFactory(); + const factory = new CascadingConfigArrayFactory({ + configArrayFactory, + getEslintAllConfig, + getEslintRecommendedConfig + }); - it("should not load disabled environments", () => { - const configPath = path.resolve(dirname, "../fixtures/environments/disable.yaml"); - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: configPath, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath - }); - const config = getConfig(factory, configPath); + sinon.spy(configArrayFactory, "loadInDirectory"); - assert.isUndefined(config.globals.window); - }); + // If cached this should be called only once + getConfig(factory, configPath); + const callcount = configArrayFactory.loadInDirectory.callcount; + + getConfig(factory, configPath); - it("should gracefully handle empty files", () => { - const configPath = path.resolve(dirname, "../fixtures/configurations/env-node.json"); - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: configPath, - eslintAllPath, - eslintRecommendedPath + assert.strictEqual(configArrayFactory.loadInDirectory.callcount, callcount); }); - getConfig(factory, path.resolve(dirname, "../fixtures/configurations/empty/empty.json")); - }); + // make sure JS-style comments don't throw an error + it("should load the config file when there are JS-style comments in the text", () => { + const specificConfigPath = path.resolve(dirname, "../fixtures/configurations/comments.json"); + const factory = new CascadingConfigArrayFactory({ + specificConfigPath, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + const config = getConfig(factory); + const { semi, strict } = config.rules; - // Meaningful stack-traces - it("should include references to where an `extends` configuration was loaded from", () => { - const configPath = path.resolve(dirname, "../fixtures/config-extends/error.json"); + assert.deepStrictEqual(semi, [1]); + assert.deepStrictEqual(strict, [0]); + }); - assert.throws(() => { + // make sure YAML files work correctly + it("should load the config file when a YAML file is used", () => { + const specificConfigPath = path.resolve(dirname, "../fixtures/configurations/env-browser.yaml"); const factory = new CascadingConfigArrayFactory({ + specificConfigPath, useEslintrc: false, - specificConfigPath: configPath, - eslintAllPath, - eslintRecommendedPath + getEslintAllConfig, + getEslintRecommendedConfig }); + const config = getConfig(factory); + const { "no-alert": noAlert, "no-undef": noUndef } = config.rules; - getConfig(factory, configPath); - }, /Referenced from:.*?error\.json/u); - }); + assert.deepStrictEqual(noAlert, [0]); + assert.deepStrictEqual(noUndef, [2]); + }); - // Keep order with the last array element taking highest precedence - it("should make the last element in an array take the highest precedence", () => { - const configPath = path.resolve(dirname, "../fixtures/config-extends/array/.eslintrc"); - const factory = new CascadingConfigArrayFactory({ - useEslintrc: false, - specificConfigPath: configPath, - eslintAllPath, - eslintRecommendedPath - }); - const expected = { - rules: { "no-empty": [1], "comma-dangle": [2], "no-console": [2] }, - env: { browser: false, node: true, es6: true }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, configPath); + it("should contain the correct value for parser when a custom parser is specified", () => { + const configPath = path.resolve(dirname, "../fixtures/configurations/parser/.eslintrc.json"); + const factory = new CascadingConfigArrayFactory({ + getEslintAllConfig, + getEslintRecommendedConfig + }); + const config = getConfig(factory, configPath); - assertConfigsEqual(actual, expected); - }); + assert.strictEqual(config.parser, path.resolve(path.dirname(configPath), "./custom.cjs")); + }); - describe("with env in a child configuration file", () => { - it("should not overwrite parserOptions of the parent with env of the child", () => { + /* + * Configuration hierarchy --------------------------------------------- + * https://github.com/eslint/eslint/issues/3915 + */ + it("should correctly merge environment settings", () => { const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - eslintRecommendedPath + useEslintrc: true, + getEslintAllConfig, + getEslintRecommendedConfig }); - const targetPath = getFixturePath("overwrite-ecmaFeatures", "child", "foo.js"); + const file = getFixturePath("envs", "sub", "foo.js"); const expected = { rules: {}, - env: { commonjs: true }, - parserOptions: { ecmaFeatures: { globalReturn: false } }, + env: { + browser: true, + node: false + }, ignorePatterns: cwdIgnorePatterns }; - const actual = getConfig(factory, targetPath); + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - }); - - describe("personal config file within home directory", () => { - - const root = path.join(systemTempDir, "eslint/cli-engine/cascading-config-array-factory/personal-config"); - - const { prepare, cleanup, getPath } = createCustomTeardown({ - cwd: root, - files: { - ...DIRECTORY_CONFIG_HIERARCHY - } - }); - - before(prepare); - after(cleanup); - - /** - * Returns the path inside of the fixture directory. - * @param {...string} args file path segments. - * @returns {string} The path inside the fixture directory. - * @private - */ - function getFakeFixturePath(...args) { - return path.join(getPath(), "eslint", "fixtures", "config-hierarchy", ...args); - } - it("should load the personal config if no local config was found", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "home-folder"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + // Default configuration - blank + it("should return a blank config when using no .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: projectPath, - eslintAllPath, - eslintRecommendedPath + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig }); - - mockOsHomedir(homePath); - - const actual = getConfig(factory, filePath); + const file = getFixturePath("broken", "console-wrong-quotes.js"); const expected = { - rules: { - "home-folder-rule": [2] - } + rules: {}, + globals: {}, + env: {}, + ignorePatterns: cwdIgnorePatterns }; + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - it("should ignore the personal config if a local config was found", () => { - const projectPath = getFakeFixturePath("personal-config", "home-folder", "project"); - const homePath = getFakeFixturePath("personal-config", "home-folder"); - const filePath = getFakeFixturePath("personal-config", "home-folder", "project", "foo.js"); + it("should return a blank config when baseConfig is set to false and no .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: projectPath, - eslintAllPath, - eslintRecommendedPath + baseConfig: false, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig }); - - mockOsHomedir(homePath); - - const actual = getConfig(factory, filePath); + const file = getFixturePath("broken", "console-wrong-quotes.js"); const expected = { - rules: { - "project-level-rule": [2] - } + rules: {}, + globals: {}, + env: {}, + ignorePatterns: cwdIgnorePatterns }; + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - it("should ignore the personal config if config is passed through cli", () => { - const configPath = getFakeFixturePath("quotes-error.json"); - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "home-folder"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + // No default configuration + it("should return an empty config when not using .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: projectPath, - specificConfigPath: configPath, - eslintAllPath, - eslintRecommendedPath + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const actual = getConfig(factory, file); - mockOsHomedir(homePath); + assertConfigsEqual(actual, { ignorePatterns: cwdIgnorePatterns }); + }); - const actual = getConfig(factory, filePath); + it("should return a modified config when baseConfig is set to an object and no .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + baseConfig: { + env: { + node: true + }, + rules: { + quotes: [2, "single"] + } + }, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); const expected = { + env: { + node: true + }, rules: { - quotes: [2, "double"] - } + quotes: [2, "single"] + }, + ignorePatterns: cwdIgnorePatterns }; + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - it("should still load the project config if the current working directory is the same as the home folder", () => { - const projectPath = getFakeFixturePath("personal-config", "project-with-config"); - const filePath = getFakeFixturePath("personal-config", "project-with-config", "subfolder", "foo.js"); + it("should return a modified config without plugin rules enabled when baseConfig is set to an object with plugin and no .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: projectPath, - eslintAllPath, - eslintRecommendedPath + baseConfig: { + env: { + node: true + }, + rules: { + quotes: [2, "single"] + }, + plugins: ["example-with-rules-config"] + }, + cwd: getFixturePath("plugins"), + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig }); - - mockOsHomedir(projectPath); - - const actual = getConfig(factory, filePath); + const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"); const expected = { + env: { + node: true + }, + plugins: ["example-with-rules-config"], rules: { - "project-level-rule": [2], - "subfolder-level-rule": [2] + quotes: [2, "single"] } }; + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - }); - - describe("when no local or personal config is found", () => { - - const root = path.join(systemTempDir, "eslint/cli-engine/cascading-config-array-factory/personal-config"); - - const { prepare, cleanup, getPath } = createCustomTeardown({ - cwd: root, - files: { - ...DIRECTORY_CONFIG_HIERARCHY - } - }); - - before(prepare); - after(cleanup); - - /** - * Returns the path inside of the fixture directory. - * @param {...string} args file path segments. - * @returns {string} The path inside the fixture directory. - * @private - */ - function getFakeFixturePath(...args) { - return path.join(getPath(), "eslint", "fixtures", "config-hierarchy", ...args); - } - it("should throw an error if no local config and no personal config was found", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + // Project configuration - second level .eslintrc + it("should merge configs when local .eslintrc overrides parent .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: projectPath, - eslintAllPath, - eslintRecommendedPath + getEslintAllConfig, + getEslintRecommendedConfig }); + const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + "no-console": [1], + quotes: [2, "single"] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - mockOsHomedir(homePath); - - assert.throws(() => { - getConfig(factory, filePath); - }, "No ESLint configuration found"); + assertConfigsEqual(actual, expected); }); - it("should throw an error if no local config was found and ~/package.json contains no eslintConfig section", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "home-folder-with-packagejson"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + // Project configuration - third level .eslintrc + it("should merge configs when local .eslintrc overrides parent and grandparent .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: projectPath, - eslintAllPath, - eslintRecommendedPath + getEslintAllConfig, + getEslintRecommendedConfig }); + const file = getFixturePath("broken", "subbroken", "subsubbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + "no-console": [0], + quotes: [1, "double"] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - mockOsHomedir(homePath); - - assert.throws(() => { - getConfig(factory, filePath); - }, "No ESLint configuration found"); + assertConfigsEqual(actual, expected); }); - it("should not throw an error if no local config and no personal config was found but useEslintrc is false", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + // Project configuration - root set in second level .eslintrc + it("should not return or traverse configurations in parents of config with root:true", () => { const factory = new CascadingConfigArrayFactory({ - cwd: projectPath, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + getEslintAllConfig, + getEslintRecommendedConfig }); - - mockOsHomedir(homePath); - - getConfig(factory, filePath); - }); - - it("should not throw an error if no local config and no personal config was found but rules are specified", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - const factory = new CascadingConfigArrayFactory({ - cliConfig: { - rules: { quotes: [2, "single"] } + const file = getFixturePath("root-true", "parent", "root", "wrong-semi.js"); + const expected = { + rules: { + semi: [2, "never"] }, - cwd: projectPath, - eslintAllPath, - eslintRecommendedPath - }); - - mockOsHomedir(homePath); + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - getConfig(factory, filePath); + assertConfigsEqual(actual, expected); }); - it("should not throw an error if no local config and no personal config was found but baseConfig is specified", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + // Project configuration - root set in second level .eslintrc + it("should return project config when called with a relative path from a subdir", () => { const factory = new CascadingConfigArrayFactory({ - baseConfig: {}, - cwd: projectPath, - eslintAllPath, - eslintRecommendedPath + cwd: getFixturePath("root-true", "parent", "root", "subdir"), + getEslintAllConfig, + getEslintRecommendedConfig }); + const dir = "."; + const expected = { + rules: { + semi: [2, "never"] + } + }; + const actual = getConfig(factory, dir); - mockOsHomedir(homePath); - - getConfig(factory, filePath); - }); - }); - - describe("with overrides", () => { - - const root = path.join(systemTempDir, "eslint/cli-engine/cascading-config-array-factory/personal-config"); - - const { prepare, cleanup, getPath } = createCustomTeardown({ - cwd: root, - files: { - ...DIRECTORY_CONFIG_HIERARCHY - } + assertConfigsEqual(actual, expected); }); - before(prepare); - after(cleanup); - - /** - * Returns the path inside of the fixture directory. - * @param {...string} pathSegments One or more path segments, in order of depth, shallowest first - * @returns {string} The path inside the fixture directory. - * @private - */ - function getFakeFixturePath(...pathSegments) { - return path.join(getPath(), "eslint", "fixtures", "config-hierarchy", ...pathSegments); - } - - it("should merge override config when the pattern matches the file name", () => { + // Command line configuration - --config with first level .eslintrc + it("should merge command line config when config file adds to local .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: getPath(), - eslintAllPath, - eslintRecommendedPath + specificConfigPath: getFixturePath("broken", "add-conf.yaml"), + getEslintAllConfig, + getEslintRecommendedConfig }); - const targetPath = getFakeFixturePath("overrides", "foo.js"); + const file = getFixturePath("broken", "console-wrong-quotes.js"); const expected = { + env: { + node: true + }, rules: { - quotes: [2, "single"], - "no-else-return": [0], - "no-unused-vars": [1], + quotes: [2, "double"], semi: [1, "never"] - } + }, + ignorePatterns: cwdIgnorePatterns }; - const actual = getConfig(factory, targetPath); + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - it("should merge override config when the pattern matches the file path relative to the config file", () => { + // Command line configuration - --config with first level .eslintrc + it("should merge command line config when config file overrides local .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: getPath(), - eslintAllPath, - eslintRecommendedPath + specificConfigPath: getFixturePath("broken", "override-conf.yaml"), + getEslintAllConfig, + getEslintRecommendedConfig }); - const targetPath = getFakeFixturePath("overrides", "child", "child-one.js"); + const file = getFixturePath("broken", "console-wrong-quotes.js"); const expected = { + env: { + node: true + }, rules: { - curly: ["error", "multi", "consistent"], - "no-else-return": [0], - "no-unused-vars": [1], - quotes: [2, "double"], - semi: [1, "never"] - } + quotes: [0, "double"] + }, + ignorePatterns: cwdIgnorePatterns }; - const actual = getConfig(factory, targetPath); + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - it("should not merge override config when the pattern matches the absolute file path", () => { - const resolvedPath = path.resolve(dirname, "../fixtures/config-hierarchy/overrides/bar.cjs"); - - assert.throws(() => new CascadingConfigArrayFactory({ - cwd: getPath(), - baseConfig: { - overrides: [{ - files: resolvedPath, - rules: { - quotes: [1, "double"] - } - }] + // Command line configuration - --config with second level .eslintrc + it("should merge command line config when config file adds to local and parent .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: getFixturePath("broken", "add-conf.yaml"), + getEslintAllConfig, + getEslintRecommendedConfig + }); + const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true }, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath - }), /Invalid override pattern/u); - }); - - it("should not merge override config when the pattern traverses up the directory tree", () => { - const parentPath = "overrides/../**/*.js"; - - assert.throws(() => new CascadingConfigArrayFactory({ - baseConfig: { - overrides: [{ - files: parentPath, - rules: { - quotes: [1, "single"] - } - }] + rules: { + quotes: [2, "single"], + "no-console": [1], + semi: [1, "never"] }, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath - }), /Invalid override pattern/u); + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); }); - it("should merge all local configs (override and non-override) before non-local configs", () => { + // Command line configuration - --config with second level .eslintrc + it("should merge command line config when config file overrides local and parent .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: getPath(), - eslintAllPath, - eslintRecommendedPath + specificConfigPath: getFixturePath("broken", "override-conf.yaml"), + getEslintAllConfig, + getEslintRecommendedConfig }); - const targetPath = getFakeFixturePath("overrides", "two", "child-two.js"); + const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); const expected = { + env: { + node: true + }, rules: { - "no-console": [0], - "no-else-return": [0], - "no-unused-vars": [2], - quotes: [2, "double"], - semi: [2, "never"] - } + quotes: [0, "single"], + "no-console": [1] + }, + ignorePatterns: cwdIgnorePatterns }; - const actual = getConfig(factory, targetPath); + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - it("should apply overrides in parent .eslintrc over non-override rules in child .eslintrc", () => { - const targetPath = getFakeFixturePath("overrides", "three", "foo.js"); + // Command line configuration - --rule with --config and first level .eslintrc + it("should merge command line config and rule when rule and config file overrides local .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [ - { - files: "three/**/*.js", - rules: { - "semi-style": [2, "last"] - } - } - ] + cliConfig: { + rules: { + quotes: [1, "double"] + } }, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + specificConfigPath: getFixturePath("broken", "override-conf.yaml"), + getEslintAllConfig, + getEslintRecommendedConfig }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); const expected = { + env: { + node: true + }, rules: { - "semi-style": [2, "last"] - } + quotes: [1, "double"] + }, + ignorePatterns: cwdIgnorePatterns }; - const actual = getConfig(factory, targetPath); + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - it("should apply overrides if all glob patterns match", () => { - const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + // Command line configuration - --plugin + it("should merge command line plugin with local .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [{ - files: ["one/**/*", "*.js"], - rules: { - quotes: [2, "single"] - } - }] + cliConfig: { + plugins: ["another-plugin"] }, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + cwd: getFixturePath("plugins"), + resolvePluginsRelativeTo: getFixturePath("plugins"), + getEslintAllConfig, + getEslintRecommendedConfig }); + const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"); const expected = { + env: { + node: true + }, + plugins: [ + "example", + "another-plugin" + ], rules: { - quotes: [2, "single"] + quotes: [2, "double"] } }; - const actual = getConfig(factory, targetPath); + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - it("should apply overrides even if some glob patterns do not match", () => { - const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + + it("should merge multiple different config file formats", () => { const factory = new CascadingConfigArrayFactory({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [{ - files: ["one/**/*", "*two.js"], - rules: { - quotes: [2, "single"] - } - }] - }, - useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + getEslintAllConfig, + getEslintRecommendedConfig }); + const file = getFixturePath("fileexts/subdir/subsubdir/foo.js"); const expected = { + env: { + browser: true + }, rules: { - quotes: [2, "single"] - } + semi: [2, "always"], + eqeqeq: [2] + }, + ignorePatterns: cwdIgnorePatterns }; - const actual = getConfig(factory, targetPath); + const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - it("should not apply overrides if any excluded glob patterns match", () => { - const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + + it("should load user config globals", () => { + const configPath = path.resolve(dirname, "../fixtures/globals/conf.yaml"); const factory = new CascadingConfigArrayFactory({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [{ - files: "one/**/*", - excludedFiles: ["two/**/*", "*one.js"], - rules: { - quotes: [2, "single"] - } - }] - }, + specificConfigPath: configPath, useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + getEslintAllConfig, + getEslintRecommendedConfig }); const expected = { - rules: {} + globals: { + foo: true + }, + ignorePatterns: cwdIgnorePatterns }; - const actual = getConfig(factory, targetPath); + const actual = getConfig(factory, configPath); assertConfigsEqual(actual, expected); }); - it("should apply overrides if all excluded glob patterns fail to match", () => { - const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + it("should not load disabled environments", () => { + const configPath = path.resolve(dirname, "../fixtures/environments/disable.yaml"); const factory = new CascadingConfigArrayFactory({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [{ - files: "one/**/*", - excludedFiles: ["two/**/*", "*two.js"], - rules: { - quotes: [2, "single"] - } - }] - }, + specificConfigPath: configPath, useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + getEslintAllConfig, + getEslintRecommendedConfig }); - const expected = { - rules: { - quotes: [2, "single"] - } - }; - const actual = getConfig(factory, targetPath); + const config = getConfig(factory, configPath); - assertConfigsEqual(actual, expected); + assert.isUndefined(config.globals.window); }); - it("should cascade", () => { - const targetPath = getFakeFixturePath("overrides", "foo.js"); + it("should gracefully handle empty files", () => { + const configPath = path.resolve(dirname, "../fixtures/configurations/env-node.json"); + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: configPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + getConfig(factory, path.resolve(dirname, "../fixtures/configurations/empty/empty.json")); + }); + + // Meaningful stack-traces + it("should include references to where an `extends` configuration was loaded from", () => { + const configPath = path.resolve(dirname, "../fixtures/config-extends/error.json"); + + assert.throws(() => { + const factory = new CascadingConfigArrayFactory({ + useEslintrc: false, + specificConfigPath: configPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + getConfig(factory, configPath); + }, /Referenced from:.*?error\.json/u); + }); + + // Keep order with the last array element taking highest precedence + it("should make the last element in an array take the highest precedence", () => { + const configPath = path.resolve(dirname, "../fixtures/config-extends/array/.eslintrc"); const factory = new CascadingConfigArrayFactory({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [ - { - files: "foo.js", - rules: { - semi: [2, "never"], - quotes: [2, "single"] - } - }, - { - files: "foo.js", - rules: { - semi: [2, "never"], - quotes: [2, "double"] - } - } - ] - }, useEslintrc: false, - eslintAllPath, - eslintRecommendedPath + specificConfigPath: configPath, + getEslintAllConfig, + getEslintRecommendedConfig }); const expected = { - rules: { - semi: [2, "never"], - quotes: [2, "double"] - } + rules: { "no-empty": [1], "comma-dangle": [2], "no-console": [2] }, + env: { browser: false, node: true, es6: true }, + ignorePatterns: cwdIgnorePatterns }; - const actual = getConfig(factory, targetPath); + const actual = getConfig(factory, configPath); assertConfigsEqual(actual, expected); }); - }); - describe("deprecation warnings", () => { - const cwd = path.resolve(dirname, "../fixtures/config-file/"); - let warning = null; + describe("with env in a child configuration file", () => { + it("should not overwrite parserOptions of the parent with env of the child", () => { + const factory = new CascadingConfigArrayFactory({ + getEslintAllConfig, + getEslintRecommendedConfig + }); + const targetPath = getFixturePath("overwrite-ecmaFeatures", "child", "foo.js"); + const expected = { + rules: {}, + env: { commonjs: true }, + parserOptions: { ecmaFeatures: { globalReturn: false } }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + }); - /** - * Store a reported warning object if that code starts with `ESLINT_`. - * @param {{code:string, message:string}} w The warning object to store. - * @returns {void} - */ - function onWarning(w) { - if (w.code.startsWith("ESLINT_")) { - warning = w; + describe("personal config file within home directory", () => { + + const root = path.join(systemTempDir, "eslint/cli-engine/cascading-config-array-factory/personal-config"); + + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root, + files: { + ...DIRECTORY_CONFIG_HIERARCHY + } + }); + + before(prepare); + after(cleanup); + + /** + * Returns the path inside of the fixture directory. + * @param {...string} args file path segments. + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFakeFixturePath(...args) { + return path.join(getPath(), "eslint", "fixtures", "config-hierarchy", ...args); } - } - /** @type {CascadingConfigArrayFactory} */ - let factory; + it("should load the personal config if no local config was found", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "home-folder"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + mockOsHomedir(homePath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + "home-folder-rule": [2] + } + }; - beforeEach(() => { - factory = new CascadingConfigArrayFactory({ - cwd, - eslintAllPath, - eslintRecommendedPath + assertConfigsEqual(actual, expected); + }); + + it("should ignore the personal config if a local config was found", () => { + const projectPath = getFakeFixturePath("personal-config", "home-folder", "project"); + const homePath = getFakeFixturePath("personal-config", "home-folder"); + const filePath = getFakeFixturePath("personal-config", "home-folder", "project", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + mockOsHomedir(homePath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + "project-level-rule": [2] + } + }; + + assertConfigsEqual(actual, expected); + }); + + it("should ignore the personal config if config is passed through cli", () => { + const configPath = getFakeFixturePath("quotes-error.json"); + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "home-folder"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + specificConfigPath: configPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + mockOsHomedir(homePath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + quotes: [2, "double"] + } + }; + + assertConfigsEqual(actual, expected); + }); + + it("should still load the project config if the current working directory is the same as the home folder", () => { + const projectPath = getFakeFixturePath("personal-config", "project-with-config"); + const filePath = getFakeFixturePath("personal-config", "project-with-config", "subfolder", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + mockOsHomedir(projectPath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + "project-level-rule": [2], + "subfolder-level-rule": [2] + } + }; + + assertConfigsEqual(actual, expected); }); - warning = null; - process.on("warning", onWarning); }); - afterEach(() => { - process.removeListener("warning", onWarning); + + describe("when no local or personal config is found", () => { + + const root = path.join(systemTempDir, "eslint/cli-engine/cascading-config-array-factory/personal-config"); + + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root, + files: { + ...DIRECTORY_CONFIG_HIERARCHY + } + }); + + before(prepare); + after(cleanup); + + /** + * Returns the path inside of the fixture directory. + * @param {...string} args file path segments. + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFakeFixturePath(...args) { + return path.join(getPath(), "eslint", "fixtures", "config-hierarchy", ...args); + } + + it("should throw an error if no local config and no personal config was found", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + mockOsHomedir(homePath); + + assert.throws(() => { + getConfig(factory, filePath); + }, "No ESLint configuration found"); + }); + + it("should throw an error if no local config was found and ~/package.json contains no eslintConfig section", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "home-folder-with-packagejson"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + mockOsHomedir(homePath); + + assert.throws(() => { + getConfig(factory, filePath); + }, "No ESLint configuration found"); + }); + + it("should not throw an error if no local config and no personal config was found but useEslintrc is false", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + mockOsHomedir(homePath); + + getConfig(factory, filePath); + }); + + it("should not throw an error if no local config and no personal config was found but rules are specified", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cliConfig: { + rules: { quotes: [2, "single"] } + }, + cwd: projectPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + mockOsHomedir(homePath); + + getConfig(factory, filePath); + }); + + it("should not throw an error if no local config and no personal config was found but baseConfig is specified", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + baseConfig: {}, + cwd: projectPath, + getEslintAllConfig, + getEslintRecommendedConfig + }); + + mockOsHomedir(homePath); + + getConfig(factory, filePath); + }); }); - it("should emit a deprecation warning if 'ecmaFeatures' is given.", async () => { - getConfig(factory, "ecma-features/test.js"); + describe("with overrides", () => { - // Wait for "warning" event. - await nextTick(); + const root = path.join(systemTempDir, "eslint/cli-engine/cascading-config-array-factory/personal-config"); - assert.notStrictEqual(warning, null); - assert.strictEqual( - warning.message, - `The 'ecmaFeatures' config file property is deprecated and has no effect. (found in "ecma-features${path.sep}.eslintrc.yml")` - ); + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root, + files: { + ...DIRECTORY_CONFIG_HIERARCHY + } + }); + + before(prepare); + after(cleanup); + + /** + * Returns the path inside of the fixture directory. + * @param {...string} pathSegments One or more path segments, in order of depth, shallowest first + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFakeFixturePath(...pathSegments) { + return path.join(getPath(), "eslint", "fixtures", "config-hierarchy", ...pathSegments); + } + + it("should merge override config when the pattern matches the file name", () => { + const factory = new CascadingConfigArrayFactory({ + cwd: getPath(), + getEslintAllConfig, + getEslintRecommendedConfig + }); + const targetPath = getFakeFixturePath("overrides", "foo.js"); + const expected = { + rules: { + quotes: [2, "single"], + "no-else-return": [0], + "no-unused-vars": [1], + semi: [1, "never"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should merge override config when the pattern matches the file path relative to the config file", () => { + const factory = new CascadingConfigArrayFactory({ + cwd: getPath(), + getEslintAllConfig, + getEslintRecommendedConfig + }); + const targetPath = getFakeFixturePath("overrides", "child", "child-one.js"); + const expected = { + rules: { + curly: ["error", "multi", "consistent"], + "no-else-return": [0], + "no-unused-vars": [1], + quotes: [2, "double"], + semi: [1, "never"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should not merge override config when the pattern matches the absolute file path", () => { + const resolvedPath = path.resolve(dirname, "../fixtures/config-hierarchy/overrides/bar.cjs"); + + assert.throws(() => new CascadingConfigArrayFactory({ + cwd: getPath(), + baseConfig: { + overrides: [{ + files: resolvedPath, + rules: { + quotes: [1, "double"] + } + }] + }, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }), /Invalid override pattern/u); + }); + + it("should not merge override config when the pattern traverses up the directory tree", () => { + const parentPath = "overrides/../**/*.js"; + + assert.throws(() => new CascadingConfigArrayFactory({ + baseConfig: { + overrides: [{ + files: parentPath, + rules: { + quotes: [1, "single"] + } + }] + }, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }), /Invalid override pattern/u); + }); + + it("should merge all local configs (override and non-override) before non-local configs", () => { + const factory = new CascadingConfigArrayFactory({ + cwd: getPath(), + getEslintAllConfig, + getEslintRecommendedConfig + }); + const targetPath = getFakeFixturePath("overrides", "two", "child-two.js"); + const expected = { + rules: { + "no-console": [0], + "no-else-return": [0], + "no-unused-vars": [2], + quotes: [2, "double"], + semi: [2, "never"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides in parent .eslintrc over non-override rules in child .eslintrc", () => { + const targetPath = getFakeFixturePath("overrides", "three", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [ + { + files: "three/**/*.js", + rules: { + "semi-style": [2, "last"] + } + } + ] + }, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + const expected = { + rules: { + "semi-style": [2, "last"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides if all glob patterns match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: ["one/**/*", "*.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + const expected = { + rules: { + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides even if some glob patterns do not match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: ["one/**/*", "*two.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + const expected = { + rules: { + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should not apply overrides if any excluded glob patterns match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: "one/**/*", + excludedFiles: ["two/**/*", "*one.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + const expected = { + rules: {} + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides if all excluded glob patterns fail to match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: "one/**/*", + excludedFiles: ["two/**/*", "*two.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + const expected = { + rules: { + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should cascade", () => { + const targetPath = getFakeFixturePath("overrides", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [ + { + files: "foo.js", + rules: { + semi: [2, "never"], + quotes: [2, "single"] + } + }, + { + files: "foo.js", + rules: { + semi: [2, "never"], + quotes: [2, "double"] + } + } + ] + }, + useEslintrc: false, + getEslintAllConfig, + getEslintRecommendedConfig + }); + const expected = { + rules: { + semi: [2, "never"], + quotes: [2, "double"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); }); }); }); @@ -1943,66 +3148,134 @@ describe("CascadingConfigArrayFactory", () => { }; const { prepare, cleanup, getPath } = createCustomTeardown({ cwd: root, files }); - /** @type {Map} */ - let additionalPluginPool; + describe("with eslint built-in config paths", () => { - /** @type {CascadingConfigArrayFactory} */ - let factory; + /** @type {Map} */ + let additionalPluginPool; - beforeEach(async () => { - await prepare(); - additionalPluginPool = new Map(); - factory = new CascadingConfigArrayFactory({ - cwd: getPath(), - additionalPluginPool, - cliConfig: { plugins: ["test"] }, - eslintAllPath, - eslintRecommendedPath + /** @type {CascadingConfigArrayFactory} */ + let factory; + + beforeEach(async () => { + await prepare(); + additionalPluginPool = new Map(); + factory = new CascadingConfigArrayFactory({ + cwd: getPath(), + additionalPluginPool, + cliConfig: { plugins: ["test"] }, + eslintAllPath, + eslintRecommendedPath + }); }); - }); - afterEach(cleanup); + afterEach(cleanup); - it("should use cached instance.", () => { - const one = factory.getConfigArrayForFile("a.js"); - const two = factory.getConfigArrayForFile("a.js"); + it("should use cached instance.", () => { + const one = factory.getConfigArrayForFile("a.js"); + const two = factory.getConfigArrayForFile("a.js"); - assert.strictEqual(one, two); - }); + assert.strictEqual(one, two); + }); - it("should not use cached instance if 'clearCache()' method is called after first config is retrieved", () => { - const one = factory.getConfigArrayForFile("a.js"); + it("should not use cached instance if 'clearCache()' method is called after first config is retrieved", () => { + const one = factory.getConfigArrayForFile("a.js"); - factory.clearCache(); - const two = factory.getConfigArrayForFile("a.js"); + factory.clearCache(); + const two = factory.getConfigArrayForFile("a.js"); - assert.notStrictEqual(one, two); - }); + assert.notStrictEqual(one, two); + }); + + it("should have a loading error in CLI config.", () => { + const config = factory.getConfigArrayForFile("a.js"); + + assert.strictEqual(config[2].plugins.test.definition, null); + }); - it("should have a loading error in CLI config.", () => { - const config = factory.getConfigArrayForFile("a.js"); + it("should not have a loading error in CLI config after adding 'test' plugin to the additional plugin pool then calling 'clearCache()'.", () => { + factory.getConfigArrayForFile("a.js"); - assert.strictEqual(config[2].plugins.test.definition, null); + additionalPluginPool.set("test", { configs: { name: "test" } }); + factory.clearCache(); + + // Check. + const config = factory.getConfigArrayForFile("a.js"); + + assert.deepStrictEqual( + config[2].plugins.test.definition, + { + configs: { name: "test" }, + environments: {}, + processors: {}, + rules: {} + } + ); + }); }); - it("should not have a loading error in CLI config after adding 'test' plugin to the additional plugin pool then calling 'clearCache()'.", () => { - factory.getConfigArrayForFile("a.js"); + describe("with eslint built-in config callbacks", () => { - additionalPluginPool.set("test", { configs: { name: "test" } }); - factory.clearCache(); + /** @type {Map} */ + let additionalPluginPool; - // Check. - const config = factory.getConfigArrayForFile("a.js"); + /** @type {CascadingConfigArrayFactory} */ + let factory; - assert.deepStrictEqual( - config[2].plugins.test.definition, - { - configs: { name: "test" }, - environments: {}, - processors: {}, - rules: {} - } - ); + beforeEach(async () => { + await prepare(); + additionalPluginPool = new Map(); + factory = new CascadingConfigArrayFactory({ + cwd: getPath(), + additionalPluginPool, + cliConfig: { plugins: ["test"] }, + getEslintAllConfig, + getEslintRecommendedConfig + }); + }); + + afterEach(cleanup); + + it("should use cached instance.", () => { + const one = factory.getConfigArrayForFile("a.js"); + const two = factory.getConfigArrayForFile("a.js"); + + assert.strictEqual(one, two); + }); + + it("should not use cached instance if 'clearCache()' method is called after first config is retrieved", () => { + const one = factory.getConfigArrayForFile("a.js"); + + factory.clearCache(); + const two = factory.getConfigArrayForFile("a.js"); + + assert.notStrictEqual(one, two); + }); + + it("should have a loading error in CLI config.", () => { + const config = factory.getConfigArrayForFile("a.js"); + + assert.strictEqual(config[2].plugins.test.definition, null); + }); + + it("should not have a loading error in CLI config after adding 'test' plugin to the additional plugin pool then calling 'clearCache()'.", () => { + factory.getConfigArrayForFile("a.js"); + + additionalPluginPool.set("test", { configs: { name: "test" } }); + factory.clearCache(); + + // Check. + const config = factory.getConfigArrayForFile("a.js"); + + assert.deepStrictEqual( + config[2].plugins.test.definition, + { + configs: { name: "test" }, + environments: {}, + processors: {}, + rules: {} + } + ); + }); }); }); }); diff --git a/tests/lib/config-array-factory.js b/tests/lib/config-array-factory.js index 805159fd..6d9fc2c8 100644 --- a/tests/lib/config-array-factory.js +++ b/tests/lib/config-array-factory.js @@ -7,15 +7,16 @@ // Requirements //----------------------------------------------------------------------------- -import path from "path"; -import { fileURLToPath, pathToFileURL } from "url"; +import { assert } from "chai"; import fs from "fs"; import { createRequire } from "module"; -import { assert } from "chai"; +import path from "path"; import sinon from "sinon"; +import systemTempDir from "temp-dir"; +import { fileURLToPath } from "url"; + import { Legacy } from "../../lib/index.js"; import { createCustomTeardown } from "../_utils/index.js"; -import systemTempDir from "temp-dir"; const require = createRequire(import.meta.url); @@ -38,6 +39,22 @@ const eslintAllPath = path.resolve(dirname, "../fixtures/eslint-all.cjs"); const eslintRecommendedPath = path.resolve(dirname, "../fixtures/eslint-recommended.cjs"); const tempDir = path.join(systemTempDir, "eslintrc/config-array-factory"); +/** + * Return config data for built-in eslint:all. + * @returns {ConfigData} Config data + */ +function getEslintAllConfig() { + return require("../fixtures/eslint-all.cjs"); +} + +/** + * Return config data for built-in eslint:recommended. + * @returns {ConfigData} Config data + */ +function getEslintRecommendedConfig() { + return require("../fixtures/eslint-recommended.cjs"); +} + /** * Assert a config array element. * @param {Object} actual The actual value. @@ -924,328 +941,646 @@ describe("ConfigArrayFactory", () => { }); describe("'extends' details", () => { + describe("'with eslint built-in config paths", () => { + let prepare, cleanup, getPath; + + before(() => { + + ({ prepare, cleanup, getPath } = createCustomTeardown({ + cwd: tempDir, + files: { + "node_modules/eslint-config-foo/index.js": "exports.env = { browser: true }", + "node_modules/eslint-config-one/index.js": "module.exports = { extends: 'two', env: { browser: true } }", + "node_modules/eslint-config-two/index.js": "module.exports = { env: { node: true } }", + "node_modules/eslint-config-override/index.js": ` + module.exports = { + rules: { regular: 1 }, + overrides: [ + { files: '*.xxx', rules: { override: 1 } }, + { files: '*.yyy', rules: { override: 2 } } + ] + } + `, + "node_modules/eslint-plugin-foo/index.js": "exports.configs = { bar: { env: { es6: true } } }", + "node_modules/eslint-plugin-invalid-config/index.js": "exports.configs = { foo: {} }", + "node_modules/eslint-plugin-error/index.js": "throw new Error('xxx error')", + "base.js": "module.exports = { rules: { semi: [2, 'always'] } };" + } + })); - let prepare, cleanup, getPath; + factory = new ConfigArrayFactory({ + cwd: getPath(), + eslintAllPath, + eslintRecommendedPath + }); + }); - before(() => { + beforeEach(() => prepare()); + afterEach(() => cleanup()); - ({ prepare, cleanup, getPath } = createCustomTeardown({ - cwd: tempDir, - files: { - "node_modules/eslint-config-foo/index.js": "exports.env = { browser: true }", - "node_modules/eslint-config-one/index.js": "module.exports = { extends: 'two', env: { browser: true } }", - "node_modules/eslint-config-two/index.js": "module.exports = { env: { node: true } }", - "node_modules/eslint-config-override/index.js": ` - module.exports = { - rules: { regular: 1 }, - overrides: [ - { files: '*.xxx', rules: { override: 1 } }, - { files: '*.yyy', rules: { override: 2 } } - ] - } - `, - "node_modules/eslint-plugin-foo/index.js": "exports.configs = { bar: { env: { es6: true } } }", - "node_modules/eslint-plugin-invalid-config/index.js": "exports.configs = { foo: {} }", - "node_modules/eslint-plugin-error/index.js": "throw new Error('xxx error')", - "base.js": "module.exports = { rules: { semi: [2, 'always'] } };" - } - })); + it("should throw an error when extends config module is not found", () => { + assert.throws(() => { + create({ + extends: "not-exist", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "not-exist" to extend from./u); + }); - factory = new ConfigArrayFactory({ - cwd: getPath(), - eslintAllPath, - eslintRecommendedPath + it("should throw an error when an eslint config is not found", () => { + assert.throws(() => { + create({ + extends: "eslint:foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "eslint:foo" to extend from./u); }); - }); - beforeEach(() => prepare()); - afterEach(() => cleanup()); + it("should throw an error when a plugin threw while loading.", () => { + assert.throws(() => { + create({ + extends: "plugin:error/foo", + rules: { eqeqeq: 2 } + }); + }, /xxx error/u); + }); - it("should throw an error when extends config module is not found", () => { - assert.throws(() => { - create({ - extends: "not-exist", - rules: { eqeqeq: 2 } + it("should throw an error when a plugin extend is a file path.", () => { + assert.throws(() => { + create({ + extends: "plugin:./path/to/foo", + rules: { eqeqeq: 2 } + }); + }, /'extends' cannot use a file path for plugins/u); + }); + + it("should throw an error when an eslint config is not found", () => { + assert.throws(() => { + create({ + extends: "eslint:foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "eslint:foo" to extend from./u); + }); + + describe("if 'extends' property was 'eslint:all', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "eslint:all", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); }); - }, /Failed to load config "not-exist" to extend from./u); - }); - it("should throw an error when an eslint config is not found", () => { - assert.throws(() => { - create({ - extends: "eslint:foo", - rules: { eqeqeq: 2 } + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); }); - }, /Failed to load config "eslint:foo" to extend from./u); - }); - it("should throw an error when a plugin threw while loading.", () => { - assert.throws(() => { - create({ - extends: "plugin:error/foo", - rules: { eqeqeq: 2 } + it("should have the config data of 'eslint:all' at the first element.", async () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint:all", + filePath: eslintAllPath, + ...getEslintAllConfig() + }); }); - }, /xxx error/u); - }); - it("should throw an error when a plugin extend is a file path.", () => { - assert.throws(() => { - create({ - extends: "plugin:./path/to/foo", - rules: { eqeqeq: 2 } + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); }); - }, /'extends' cannot use a file path for plugins/u); - }); + }); - it("should throw an error when an eslint config is not found", () => { - assert.throws(() => { - create({ - extends: "eslint:foo", - rules: { eqeqeq: 2 } + describe("if 'extends' property was 'eslint:recommended', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "eslint:recommended", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); }); - }, /Failed to load config "eslint:foo" to extend from./u); - }); - describe("if 'extends' property was 'eslint:all', the returned value", () => { - let configArray; + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); + }); - beforeEach(() => { - configArray = create( - { extends: "eslint:all", rules: { eqeqeq: 1 } }, - { name: ".eslintrc" } - ); - }); + it("should have the config data of 'eslint:recommended' at the first element.", async () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint:recommended", + filePath: eslintRecommendedPath, + ...getEslintRecommendedConfig() + }); + }); - it("should have two elements.", () => { - assert.strictEqual(configArray.length, 2); + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); }); - it("should have the config data of 'eslint:all' at the first element.", async () => { - assertConfigArrayElement(configArray[0], { - name: ".eslintrc » eslint:all", - filePath: eslintAllPath, - ...(await import(pathToFileURL(eslintAllPath))).default + describe("if 'extends' property was 'foo', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "foo", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); }); - }); - it("should have the given config data at the second element.", () => { - assertConfigArrayElement(configArray[1], { - name: ".eslintrc", - rules: { eqeqeq: 1 } + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); }); - }); - }); - describe("if 'extends' property was 'eslint:recommended', the returned value", () => { - let configArray; + it("should have the config data of 'eslint-config-foo' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint-config-foo", + filePath: path.join(getPath(), "node_modules/eslint-config-foo/index.js"), + env: { browser: true } + }); + }); - beforeEach(() => { - configArray = create( - { extends: "eslint:recommended", rules: { eqeqeq: 1 } }, - { name: ".eslintrc" } - ); + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); }); - it("should have two elements.", () => { - assert.strictEqual(configArray.length, 2); - }); + describe("if 'extends' property was 'plugin:foo/bar', the returned value", () => { + let configArray; - it("should have the config data of 'eslint:recommended' at the first element.", async () => { - assertConfigArrayElement(configArray[0], { - name: ".eslintrc » eslint:recommended", - filePath: eslintRecommendedPath, - ...(await import(pathToFileURL(eslintRecommendedPath))).default + beforeEach(() => { + configArray = create( + { extends: "plugin:foo/bar", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); }); - }); - it("should have the given config data at the second element.", () => { - assertConfigArrayElement(configArray[1], { - name: ".eslintrc", - rules: { eqeqeq: 1 } + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); }); - }); - }); - describe("if 'extends' property was 'foo', the returned value", () => { - let configArray; + it("should have the config data of 'plugin:foo/bar' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » plugin:foo/bar", + filePath: path.join(getPath(), "node_modules/eslint-plugin-foo/index.js"), + env: { es6: true } + }); + }); - beforeEach(() => { - configArray = create( - { extends: "foo", rules: { eqeqeq: 1 } }, - { name: ".eslintrc" } - ); + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); }); - it("should have two elements.", () => { - assert.strictEqual(configArray.length, 2); - }); + describe("if 'extends' property was './base', the returned value", () => { + let configArray; - it("should have the config data of 'eslint-config-foo' at the first element.", () => { - assertConfigArrayElement(configArray[0], { - name: ".eslintrc » eslint-config-foo", - filePath: path.join(getPath(), "node_modules/eslint-config-foo/index.js"), - env: { browser: true } + beforeEach(() => { + configArray = create( + { extends: "./base", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); }); - }); - it("should have the given config data at the second element.", () => { - assertConfigArrayElement(configArray[1], { - name: ".eslintrc", - rules: { eqeqeq: 1 } + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); }); - }); - }); - describe("if 'extends' property was 'plugin:foo/bar', the returned value", () => { - let configArray; + it("should have the config data of './base' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » ./base", + filePath: path.join(getPath(), "base.js"), + rules: { semi: [2, "always"] } + }); + }); - beforeEach(() => { - configArray = create( - { extends: "plugin:foo/bar", rules: { eqeqeq: 1 } }, - { name: ".eslintrc" } - ); + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); }); - it("should have two elements.", () => { - assert.strictEqual(configArray.length, 2); - }); + describe("if 'extends' property was 'one' and the 'one' extends 'two', the returned value", () => { + let configArray; - it("should have the config data of 'plugin:foo/bar' at the first element.", () => { - assertConfigArrayElement(configArray[0], { - name: ".eslintrc » plugin:foo/bar", - filePath: path.join(getPath(), "node_modules/eslint-plugin-foo/index.js"), - env: { es6: true } + beforeEach(() => { + configArray = create( + { extends: "one", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); }); - }); - it("should have the given config data at the second element.", () => { - assertConfigArrayElement(configArray[1], { - name: ".eslintrc", - rules: { eqeqeq: 1 } + it("should have three elements.", () => { + assert.strictEqual(configArray.length, 3); }); - }); - }); - describe("if 'extends' property was './base', the returned value", () => { - let configArray; + it("should have the config data of 'eslint-config-two' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint-config-one » eslint-config-two", + filePath: path.join(getPath(), "node_modules/eslint-config-two/index.js"), + env: { node: true } + }); + }); - beforeEach(() => { - configArray = create( - { extends: "./base", rules: { eqeqeq: 1 } }, - { name: ".eslintrc" } - ); - }); + it("should have the config data of 'eslint-config-one' at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc » eslint-config-one", + filePath: path.join(getPath(), "node_modules/eslint-config-one/index.js"), + env: { browser: true } + }); + }); - it("should have two elements.", () => { - assert.strictEqual(configArray.length, 2); + it("should have the given config data at the third element.", () => { + assertConfigArrayElement(configArray[2], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); }); - it("should have the config data of './base' at the first element.", () => { - assertConfigArrayElement(configArray[0], { - name: ".eslintrc » ./base", - filePath: path.join(getPath(), "base.js"), - rules: { semi: [2, "always"] } + describe("if 'extends' property was 'override' and the 'override' has 'overrides' property, the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "override", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); }); - }); - it("should have the given config data at the second element.", () => { - assertConfigArrayElement(configArray[1], { - name: ".eslintrc", - rules: { eqeqeq: 1 } + it("should have four elements.", () => { + assert.strictEqual(configArray.length, 4); + }); + + it("should have the config data of 'eslint-config-override' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint-config-override", + filePath: path.join(getPath(), "node_modules/eslint-config-override/index.js"), + rules: { regular: 1 } + }); + }); + + it("should have the 'overrides[0]' config data of 'eslint-config-override' at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc » eslint-config-override#overrides[0]", + filePath: path.join(getPath(), "node_modules/eslint-config-override/index.js"), + criteria: OverrideTester.create(["*.xxx"], [], getPath()), + rules: { override: 1 } + }); + }); + + it("should have the 'overrides[1]' config data of 'eslint-config-override' at the third element.", () => { + assertConfigArrayElement(configArray[2], { + name: ".eslintrc » eslint-config-override#overrides[1]", + filePath: path.join(getPath(), "node_modules/eslint-config-override/index.js"), + criteria: OverrideTester.create(["*.yyy"], [], tempDir), + rules: { override: 2 } + }); + }); + + it("should have the given config data at the fourth element.", () => { + assertConfigArrayElement(configArray[3], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); }); }); }); - describe("if 'extends' property was 'one' and the 'one' extends 'two', the returned value", () => { - let configArray; + describe("'with eslint built-in config callbacks", () => { + let prepare, cleanup, getPath; - beforeEach(() => { - configArray = create( - { extends: "one", rules: { eqeqeq: 1 } }, - { name: ".eslintrc" } - ); - }); + before(() => { - it("should have three elements.", () => { - assert.strictEqual(configArray.length, 3); - }); + ({ prepare, cleanup, getPath } = createCustomTeardown({ + cwd: tempDir, + files: { + "node_modules/eslint-config-foo/index.js": "exports.env = { browser: true }", + "node_modules/eslint-config-one/index.js": "module.exports = { extends: 'two', env: { browser: true } }", + "node_modules/eslint-config-two/index.js": "module.exports = { env: { node: true } }", + "node_modules/eslint-config-override/index.js": ` + module.exports = { + rules: { regular: 1 }, + overrides: [ + { files: '*.xxx', rules: { override: 1 } }, + { files: '*.yyy', rules: { override: 2 } } + ] + } + `, + "node_modules/eslint-plugin-foo/index.js": "exports.configs = { bar: { env: { es6: true } } }", + "node_modules/eslint-plugin-invalid-config/index.js": "exports.configs = { foo: {} }", + "node_modules/eslint-plugin-error/index.js": "throw new Error('xxx error')", + "base.js": "module.exports = { rules: { semi: [2, 'always'] } };" + } + })); - it("should have the config data of 'eslint-config-two' at the first element.", () => { - assertConfigArrayElement(configArray[0], { - name: ".eslintrc » eslint-config-one » eslint-config-two", - filePath: path.join(getPath(), "node_modules/eslint-config-two/index.js"), - env: { node: true } + factory = new ConfigArrayFactory({ + cwd: getPath(), + getEslintAllConfig, + getEslintRecommendedConfig }); }); - it("should have the config data of 'eslint-config-one' at the second element.", () => { - assertConfigArrayElement(configArray[1], { - name: ".eslintrc » eslint-config-one", - filePath: path.join(getPath(), "node_modules/eslint-config-one/index.js"), - env: { browser: true } - }); + beforeEach(() => prepare()); + afterEach(() => cleanup()); + + it("should throw an error when extends config module is not found", () => { + assert.throws(() => { + create({ + extends: "not-exist", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "not-exist" to extend from./u); }); - it("should have the given config data at the third element.", () => { - assertConfigArrayElement(configArray[2], { - name: ".eslintrc", - rules: { eqeqeq: 1 } - }); + it("should throw an error when an eslint config is not found", () => { + assert.throws(() => { + create({ + extends: "eslint:foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "eslint:foo" to extend from./u); }); - }); - describe("if 'extends' property was 'override' and the 'override' has 'overrides' property, the returned value", () => { - let configArray; + it("should throw an error when a plugin threw while loading.", () => { + assert.throws(() => { + create({ + extends: "plugin:error/foo", + rules: { eqeqeq: 2 } + }); + }, /xxx error/u); + }); - beforeEach(() => { - configArray = create( - { extends: "override", rules: { eqeqeq: 1 } }, - { name: ".eslintrc" } - ); + it("should throw an error when a plugin extend is a file path.", () => { + assert.throws(() => { + create({ + extends: "plugin:./path/to/foo", + rules: { eqeqeq: 2 } + }); + }, /'extends' cannot use a file path for plugins/u); }); - it("should have four elements.", () => { - assert.strictEqual(configArray.length, 4); + it("should throw an error when an eslint config is not found", () => { + assert.throws(() => { + create({ + extends: "eslint:foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "eslint:foo" to extend from./u); }); - it("should have the config data of 'eslint-config-override' at the first element.", () => { - assertConfigArrayElement(configArray[0], { - name: ".eslintrc » eslint-config-override", - filePath: path.join(getPath(), "node_modules/eslint-config-override/index.js"), - rules: { regular: 1 } + describe("if 'extends' property was 'eslint:all', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "eslint:all", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); }); - }); - it("should have the 'overrides[0]' config data of 'eslint-config-override' at the second element.", () => { - assertConfigArrayElement(configArray[1], { - name: ".eslintrc » eslint-config-override#overrides[0]", - filePath: path.join(getPath(), "node_modules/eslint-config-override/index.js"), - criteria: OverrideTester.create(["*.xxx"], [], getPath()), - rules: { override: 1 } + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); + }); + + it("should have the config data of 'eslint:all' at the first element.", async () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint:all", + ...getEslintAllConfig() + }); + }); + + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); }); }); - it("should have the 'overrides[1]' config data of 'eslint-config-override' at the third element.", () => { - assertConfigArrayElement(configArray[2], { - name: ".eslintrc » eslint-config-override#overrides[1]", - filePath: path.join(getPath(), "node_modules/eslint-config-override/index.js"), - criteria: OverrideTester.create(["*.yyy"], [], tempDir), - rules: { override: 2 } + describe("if 'extends' property was 'eslint:recommended', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "eslint:recommended", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); + }); + + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); + }); + + it("should have the config data of 'eslint:recommended' at the first element.", async () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint:recommended", + ...getEslintRecommendedConfig() + }); + }); + + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); }); }); - it("should have the given config data at the fourth element.", () => { - assertConfigArrayElement(configArray[3], { - name: ".eslintrc", - rules: { eqeqeq: 1 } + describe("if 'extends' property was 'foo', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "foo", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); + }); + + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); + }); + + it("should have the config data of 'eslint-config-foo' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint-config-foo", + filePath: path.join(getPath(), "node_modules/eslint-config-foo/index.js"), + env: { browser: true } + }); + }); + + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); }); }); - }); - }); - describe("'overrides' details", () => { + describe("if 'extends' property was 'plugin:foo/bar', the returned value", () => { + let configArray; - const { prepare, cleanup, getPath } = createCustomTeardown({ + beforeEach(() => { + configArray = create( + { extends: "plugin:foo/bar", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); + }); + + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); + }); + + it("should have the config data of 'plugin:foo/bar' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » plugin:foo/bar", + filePath: path.join(getPath(), "node_modules/eslint-plugin-foo/index.js"), + env: { es6: true } + }); + }); + + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); + }); + + describe("if 'extends' property was './base', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "./base", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); + }); + + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); + }); + + it("should have the config data of './base' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » ./base", + filePath: path.join(getPath(), "base.js"), + rules: { semi: [2, "always"] } + }); + }); + + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); + }); + + describe("if 'extends' property was 'one' and the 'one' extends 'two', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "one", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); + }); + + it("should have three elements.", () => { + assert.strictEqual(configArray.length, 3); + }); + + it("should have the config data of 'eslint-config-two' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint-config-one » eslint-config-two", + filePath: path.join(getPath(), "node_modules/eslint-config-two/index.js"), + env: { node: true } + }); + }); + + it("should have the config data of 'eslint-config-one' at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc » eslint-config-one", + filePath: path.join(getPath(), "node_modules/eslint-config-one/index.js"), + env: { browser: true } + }); + }); + + it("should have the given config data at the third element.", () => { + assertConfigArrayElement(configArray[2], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); + }); + + describe("if 'extends' property was 'override' and the 'override' has 'overrides' property, the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "override", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); + }); + + it("should have four elements.", () => { + assert.strictEqual(configArray.length, 4); + }); + + it("should have the config data of 'eslint-config-override' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint-config-override", + filePath: path.join(getPath(), "node_modules/eslint-config-override/index.js"), + rules: { regular: 1 } + }); + }); + + it("should have the 'overrides[0]' config data of 'eslint-config-override' at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc » eslint-config-override#overrides[0]", + filePath: path.join(getPath(), "node_modules/eslint-config-override/index.js"), + criteria: OverrideTester.create(["*.xxx"], [], getPath()), + rules: { override: 1 } + }); + }); + + it("should have the 'overrides[1]' config data of 'eslint-config-override' at the third element.", () => { + assertConfigArrayElement(configArray[2], { + name: ".eslintrc » eslint-config-override#overrides[1]", + filePath: path.join(getPath(), "node_modules/eslint-config-override/index.js"), + criteria: OverrideTester.create(["*.yyy"], [], tempDir), + rules: { override: 2 } + }); + }); + + it("should have the given config data at the fourth element.", () => { + assertConfigArrayElement(configArray[3], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); + }); + }); + }); + + describe("'overrides' details", () => { + + const { prepare, cleanup, getPath } = createCustomTeardown({ cwd: tempDir, files: { "node_modules/eslint-config-foo/index.js": ` @@ -1588,217 +1923,436 @@ describe("ConfigArrayFactory", () => { "yaml/.eslintrc.yaml": "env:\n browser: true" }; const { prepare, cleanup, getPath } = createCustomTeardown({ cwd: tempDir, files }); - let factory; - beforeEach(async () => { - await prepare(); - factory = new ConfigArrayFactory({ - cwd: getPath(), - eslintAllPath, - eslintRecommendedPath + describe("with eslint built-in config paths", () => { + let factory; + + beforeEach(async () => { + await prepare(); + factory = new ConfigArrayFactory({ + cwd: getPath(), + eslintAllPath, + eslintRecommendedPath + }); }); - }); - afterEach(cleanup); + afterEach(cleanup); - /** - * Apply `extends` property. - * @param {Object} configData The config that has `extends` property. - * @param {string} [filePath] The path to the config data. - * @returns {Object} The applied config data. - */ - function applyExtends(configData, filePath = "whatever") { - return factory - .create(configData, { filePath }) - .extractConfig(filePath) - .toCompatibleObjectAsConfigFileContent(); - } + /** + * Apply `extends` property. + * @param {Object} configData The config that has `extends` property. + * @param {string} [filePath] The path to the config data. + * @returns {Object} The applied config data. + */ + function applyExtends(configData, filePath = "whatever") { + return factory + .create(configData, { filePath }) + .extractConfig(filePath) + .toCompatibleObjectAsConfigFileContent(); + } - it("should apply extension 'foo' when specified from root directory config", () => { - const config = applyExtends({ - extends: "foo", - rules: { eqeqeq: 2 } + it("should apply extension 'foo' when specified from root directory config", () => { + const config = applyExtends({ + extends: "foo", + rules: { eqeqeq: 2 } + }); + + assertConfig(config, { + env: { browser: true }, + rules: { eqeqeq: [2] } + }); }); - assertConfig(config, { - env: { browser: true }, - rules: { eqeqeq: [2] } + it("should apply all rules when extends config includes 'eslint:all'", () => { + const config = applyExtends({ + extends: "eslint:all" + }); + + assert.strictEqual(config.rules.eqeqeq[0], "error"); + assert.strictEqual(config.rules.curly[0], "error"); }); - }); - it("should apply all rules when extends config includes 'eslint:all'", () => { - const config = applyExtends({ - extends: "eslint:all" + it("should throw an error when extends config module is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "not-exist", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "not-exist" to extend from./u); }); - assert.strictEqual(config.rules.eqeqeq[0], "error"); - assert.strictEqual(config.rules.curly[0], "error"); - }); + it("should throw an error when an eslint config is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "eslint:foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "eslint:foo" to extend from./u); + }); - it("should throw an error when extends config module is not found", () => { - assert.throws(() => { - applyExtends({ - extends: "not-exist", - rules: { eqeqeq: 2 } - }); - }, /Failed to load config "not-exist" to extend from./u); - }); + it("should throw an error when a parser in a plugin config is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "plugin:invalid-parser/foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load parser 'nonexistent-parser' declared in 'whatever » plugin:invalid-parser\/foo'/u); + }); - it("should throw an error when an eslint config is not found", () => { - assert.throws(() => { - applyExtends({ - extends: "eslint:foo", - rules: { eqeqeq: 2 } + it("should fall back to default parser when a parser called 'espree' is not found", async () => { + const config = applyExtends({ parser: "espree" }); + + assertConfig(config, { + + // parser: await import.meta.resolve("espree") + parser: require.resolve("espree") }); - }, /Failed to load config "eslint:foo" to extend from./u); - }); + }); - it("should throw an error when a parser in a plugin config is not found", () => { - assert.throws(() => { - applyExtends({ - extends: "plugin:invalid-parser/foo", + it("should throw an error when a plugin config is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "plugin:invalid-config/bar", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "plugin:invalid-config\/bar" to extend from./u); + }); + + it("should throw an error with a message template when a plugin config specifier is missing config name", () => { + try { + applyExtends({ + extends: "plugin:some-plugin", + rules: { eqeqeq: 2 } + }); + } catch (err) { + assert.strictEqual(err.messageTemplate, "plugin-invalid"); + assert.deepStrictEqual(err.messageData, { + configName: "plugin:some-plugin", + importerName: path.join(getPath(), "whatever") + }); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should throw an error with a message template when a plugin referenced for a plugin config is not found", () => { + try { + applyExtends({ + extends: "plugin:nonexistent-plugin/baz", + rules: { eqeqeq: 2 } + }); + } catch (err) { + assert.strictEqual(err.messageTemplate, "plugin-missing"); + assert.deepStrictEqual(err.messageData, { + pluginName: "eslint-plugin-nonexistent-plugin", + resolvePluginsRelativeTo: getPath(), + importerName: "whatever" + }); + return; + } + + assert.fail("Expected to throw an error"); + }); + + it("should throw an error with a message template when a plugin in the plugins list is not found", () => { + try { + applyExtends({ + plugins: ["nonexistent-plugin"] + }); + } catch (err) { + assert.strictEqual(err.messageTemplate, "plugin-missing"); + assert.deepStrictEqual(err.messageData, { + pluginName: "eslint-plugin-nonexistent-plugin", + resolvePluginsRelativeTo: getPath(), + importerName: "whatever" + }); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should apply extensions recursively when specified from package", () => { + const config = applyExtends({ + extends: "one", rules: { eqeqeq: 2 } }); - }, /Failed to load parser 'nonexistent-parser' declared in 'whatever » plugin:invalid-parser\/foo'/u); - }); - it("should fall back to default parser when a parser called 'espree' is not found", async () => { - const config = applyExtends({ parser: "espree" }); + assertConfig(config, { + env: { browser: true, node: true }, + rules: { eqeqeq: [2] } + }); + }); - assertConfig(config, { + it("should apply extensions when specified from a JavaScript file", () => { + const config = applyExtends({ + extends: ".eslintrc.js", + rules: { eqeqeq: 2 } + }, "js/foo.js"); - // parser: await import.meta.resolve("espree") - parser: require.resolve("espree") + assertConfig(config, { + rules: { + semi: [2, "always"], + eqeqeq: [2] + } + }); }); - }); - it("should throw an error when a plugin config is not found", () => { - assert.throws(() => { - applyExtends({ - extends: "plugin:invalid-config/bar", + it("should apply extensions when specified from a YAML file", () => { + const config = applyExtends({ + extends: ".eslintrc.yaml", rules: { eqeqeq: 2 } + }, "yaml/foo.js"); + + assertConfig(config, { + env: { browser: true }, + rules: { + eqeqeq: [2] + } }); - }, /Failed to load config "plugin:invalid-config\/bar" to extend from./u); - }); + }); - it("should throw an error with a message template when a plugin config specifier is missing config name", () => { - try { - applyExtends({ - extends: "plugin:some-plugin", + it("should apply extensions when specified from a JSON file", () => { + const config = applyExtends({ + extends: ".eslintrc.json", rules: { eqeqeq: 2 } + }, "json/foo.js"); + + assertConfig(config, { + rules: { + eqeqeq: [2], + quotes: [2, "double"] + } }); - } catch (err) { - assert.strictEqual(err.messageTemplate, "plugin-invalid"); - assert.deepStrictEqual(err.messageData, { - configName: "plugin:some-plugin", - importerName: path.join(getPath(), "whatever") - }); - return; - } - assert.fail("Expected to throw an error"); - }); + }); - it("should throw an error with a message template when a plugin referenced for a plugin config is not found", () => { - try { - applyExtends({ - extends: "plugin:nonexistent-plugin/baz", + it("should apply extensions when specified from a package.json file in a sibling directory", () => { + const config = applyExtends({ + extends: "../package-json/package.json", rules: { eqeqeq: 2 } + }, "json/foo.js"); + + assertConfig(config, { + env: { es6: true }, + rules: { + eqeqeq: [2] + } }); - } catch (err) { - assert.strictEqual(err.messageTemplate, "plugin-missing"); - assert.deepStrictEqual(err.messageData, { - pluginName: "eslint-plugin-nonexistent-plugin", - resolvePluginsRelativeTo: getPath(), - importerName: "whatever" + }); + }); + + describe("with eslint built-in config callbacks", () => { + let factory; + + beforeEach(async () => { + await prepare(); + factory = new ConfigArrayFactory({ + cwd: getPath(), + getEslintAllConfig, + getEslintRecommendedConfig }); - return; + }); + + afterEach(cleanup); + + /** + * Apply `extends` property. + * @param {Object} configData The config that has `extends` property. + * @param {string} [filePath] The path to the config data. + * @returns {Object} The applied config data. + */ + function applyExtends(configData, filePath = "whatever") { + return factory + .create(configData, { filePath }) + .extractConfig(filePath) + .toCompatibleObjectAsConfigFileContent(); } - assert.fail("Expected to throw an error"); - }); + it("should apply extension 'foo' when specified from root directory config", () => { + const config = applyExtends({ + extends: "foo", + rules: { eqeqeq: 2 } + }); - it("should throw an error with a message template when a plugin in the plugins list is not found", () => { - try { - applyExtends({ - plugins: ["nonexistent-plugin"] + assertConfig(config, { + env: { browser: true }, + rules: { eqeqeq: [2] } }); - } catch (err) { - assert.strictEqual(err.messageTemplate, "plugin-missing"); - assert.deepStrictEqual(err.messageData, { - pluginName: "eslint-plugin-nonexistent-plugin", - resolvePluginsRelativeTo: getPath(), - importerName: "whatever" + }); + + it("should apply all rules when extends config includes 'eslint:all'", () => { + const config = applyExtends({ + extends: "eslint:all" }); - return; - } - assert.fail("Expected to throw an error"); - }); - it("should apply extensions recursively when specified from package", () => { - const config = applyExtends({ - extends: "one", - rules: { eqeqeq: 2 } + assert.strictEqual(config.rules.eqeqeq[0], "error"); + assert.strictEqual(config.rules.curly[0], "error"); }); - assertConfig(config, { - env: { browser: true, node: true }, - rules: { eqeqeq: [2] } + it("should throw an error when extends config module is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "not-exist", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "not-exist" to extend from./u); }); - }); - it("should apply extensions when specified from a JavaScript file", () => { - const config = applyExtends({ - extends: ".eslintrc.js", - rules: { eqeqeq: 2 } - }, "js/foo.js"); + it("should throw an error when an eslint config is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "eslint:foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "eslint:foo" to extend from./u); + }); - assertConfig(config, { - rules: { - semi: [2, "always"], - eqeqeq: [2] - } + it("should throw an error when a parser in a plugin config is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "plugin:invalid-parser/foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load parser 'nonexistent-parser' declared in 'whatever » plugin:invalid-parser\/foo'/u); }); - }); - it("should apply extensions when specified from a YAML file", () => { - const config = applyExtends({ - extends: ".eslintrc.yaml", - rules: { eqeqeq: 2 } - }, "yaml/foo.js"); + it("should fall back to default parser when a parser called 'espree' is not found", async () => { + const config = applyExtends({ parser: "espree" }); - assertConfig(config, { - env: { browser: true }, - rules: { - eqeqeq: [2] - } + assertConfig(config, { + + // parser: await import.meta.resolve("espree") + parser: require.resolve("espree") + }); }); - }); - it("should apply extensions when specified from a JSON file", () => { - const config = applyExtends({ - extends: ".eslintrc.json", - rules: { eqeqeq: 2 } - }, "json/foo.js"); + it("should throw an error when a plugin config is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "plugin:invalid-config/bar", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "plugin:invalid-config\/bar" to extend from./u); + }); - assertConfig(config, { - rules: { - eqeqeq: [2], - quotes: [2, "double"] + it("should throw an error with a message template when a plugin config specifier is missing config name", () => { + try { + applyExtends({ + extends: "plugin:some-plugin", + rules: { eqeqeq: 2 } + }); + } catch (err) { + assert.strictEqual(err.messageTemplate, "plugin-invalid"); + assert.deepStrictEqual(err.messageData, { + configName: "plugin:some-plugin", + importerName: path.join(getPath(), "whatever") + }); + return; } + assert.fail("Expected to throw an error"); }); - }); - it("should apply extensions when specified from a package.json file in a sibling directory", () => { - const config = applyExtends({ - extends: "../package-json/package.json", - rules: { eqeqeq: 2 } - }, "json/foo.js"); + it("should throw an error with a message template when a plugin referenced for a plugin config is not found", () => { + try { + applyExtends({ + extends: "plugin:nonexistent-plugin/baz", + rules: { eqeqeq: 2 } + }); + } catch (err) { + assert.strictEqual(err.messageTemplate, "plugin-missing"); + assert.deepStrictEqual(err.messageData, { + pluginName: "eslint-plugin-nonexistent-plugin", + resolvePluginsRelativeTo: getPath(), + importerName: "whatever" + }); + return; + } - assertConfig(config, { - env: { es6: true }, - rules: { - eqeqeq: [2] + assert.fail("Expected to throw an error"); + }); + + it("should throw an error with a message template when a plugin in the plugins list is not found", () => { + try { + applyExtends({ + plugins: ["nonexistent-plugin"] + }); + } catch (err) { + assert.strictEqual(err.messageTemplate, "plugin-missing"); + assert.deepStrictEqual(err.messageData, { + pluginName: "eslint-plugin-nonexistent-plugin", + resolvePluginsRelativeTo: getPath(), + importerName: "whatever" + }); + return; } + assert.fail("Expected to throw an error"); + }); + + it("should apply extensions recursively when specified from package", () => { + const config = applyExtends({ + extends: "one", + rules: { eqeqeq: 2 } + }); + + assertConfig(config, { + env: { browser: true, node: true }, + rules: { eqeqeq: [2] } + }); + }); + + it("should apply extensions when specified from a JavaScript file", () => { + const config = applyExtends({ + extends: ".eslintrc.js", + rules: { eqeqeq: 2 } + }, "js/foo.js"); + + assertConfig(config, { + rules: { + semi: [2, "always"], + eqeqeq: [2] + } + }); + }); + + it("should apply extensions when specified from a YAML file", () => { + const config = applyExtends({ + extends: ".eslintrc.yaml", + rules: { eqeqeq: 2 } + }, "yaml/foo.js"); + + assertConfig(config, { + env: { browser: true }, + rules: { + eqeqeq: [2] + } + }); + }); + + it("should apply extensions when specified from a JSON file", () => { + const config = applyExtends({ + extends: ".eslintrc.json", + rules: { eqeqeq: 2 } + }, "json/foo.js"); + + assertConfig(config, { + rules: { + eqeqeq: [2], + quotes: [2, "double"] + } + }); + }); + + it("should apply extensions when specified from a package.json file in a sibling directory", () => { + const config = applyExtends({ + extends: "../package-json/package.json", + rules: { eqeqeq: 2 } + }, "json/foo.js"); + + assertConfig(config, { + env: { es6: true }, + rules: { + eqeqeq: [2] + } + }); }); }); }); @@ -1809,7 +2363,7 @@ describe("ConfigArrayFactory", () => { let cleanup; beforeEach(() => { - cleanup = () => {}; + cleanup = () => { }; }); afterEach(() => cleanup());