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/tests/lib/cascading-config-array-factory.js b/tests/lib/cascading-config-array-factory.js index 74b3aad4..91ed2ea2 100644 --- a/tests/lib/cascading-config-array-factory.js +++ b/tests/lib/cascading-config-array-factory.js @@ -9,6 +9,7 @@ import { assert } from "chai"; import fs from "fs"; +import { createRequire } from "module"; import os from "os"; import path from "path"; import sh from "shelljs"; @@ -19,6 +20,8 @@ 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 { @@ -46,7 +49,7 @@ const eslintRecommendedPath = path.resolve(dirname, "../fixtures/eslint-recommen * @returns {ConfigData} Config data */ function getEslintAllConfig() { - return import("../fixtures/eslint-all.cjs"); + return require("../fixtures/eslint-all.cjs"); } /** @@ -54,7 +57,7 @@ function getEslintAllConfig() { * @returns {ConfigData} Config data */ function getEslintRecommendedConfig() { - return import("../fixtures/eslint-recommended.cjs"); + return require("../fixtures/eslint-recommended.cjs"); } //----------------------------------------------------------------------------- @@ -544,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 = (() => { @@ -577,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 @@ -640,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, - getEslintAllConfig, - 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, - getEslintRecommendedConfig + 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({ - getEslintAllConfig, - 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, - getEslintRecommendedConfig + 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({ - getEslintAllConfig, - 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, - getEslintRecommendedConfig }); - 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, - getEslintAllConfig, - 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, - getEslintRecommendedConfig + 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({ - getEslintAllConfig, - 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, - getEslintRecommendedConfig - }); - 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, - getEslintAllConfig, - 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, - getEslintRecommendedConfig - }); - 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, - getEslintAllConfig, - 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, - getEslintRecommendedConfig - }); - 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, - getEslintAllConfig, - 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); - }); + } + }; + const actual = getConfig(factory, file); - // Project configuration - second level .eslintrc - it("should merge configs when local .eslintrc overrides parent .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - getEslintRecommendedConfig + assertConfigsEqual(actual, expected); }); - 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); - }); + // 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 - third level .eslintrc - it("should merge configs when local .eslintrc overrides parent and grandparent .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - getEslintAllConfig, - 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 - 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 not return or traverse configurations in parents of config with root:true", () => { - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - getEslintRecommendedConfig + 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 - 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); - // 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"), - getEslintAllConfig, - eslintRecommendedPath + 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 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 adds to local .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: getFixturePath("broken", "add-conf.yaml"), - eslintAllPath, - getEslintRecommendedConfig + 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); - }); + // 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 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"), - getEslintAllConfig, - 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 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 adds to local and parent .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: getFixturePath("broken", "add-conf.yaml"), - eslintAllPath, - getEslintRecommendedConfig + 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 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); - // 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"), - getEslintAllConfig, - 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 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: { + // 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, - getEslintRecommendedConfig - }); - 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"), - getEslintAllConfig, - 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"] + } + }; - it("should merge multiple different config file formats", () => { - const factory = new CascadingConfigArrayFactory({ - eslintAllPath, - getEslintRecommendedConfig + 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); + }); }); - 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); - }); + 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 load user config globals", () => { - const configPath = path.resolve(dirname, "../fixtures/globals/conf.yaml"); - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: configPath, - useEslintrc: false, - getEslintAllConfig, - eslintRecommendedPath + 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); + }); }); - const expected = { - globals: { - foo: true - }, - ignorePatterns: cwdIgnorePatterns - }; - const actual = getConfig(factory, configPath); - assertConfigsEqual(actual, expected); - }); + 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 not load disabled environments", () => { - const configPath = path.resolve(dirname, "../fixtures/environments/disable.yaml"); - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: configPath, - useEslintrc: false, - eslintAllPath, - getEslintRecommendedConfig + 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); + }); }); - const config = getConfig(factory, configPath); - assert.isUndefined(config.globals.window); + 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")` + ); + }); + }); }); - it("should gracefully handle empty files", () => { - const configPath = path.resolve(dirname, "../fixtures/configurations/env-node.json"); - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: configPath, - getEslintAllConfig, - eslintRecommendedPath + describe("with eslint built-in config callbacks", () => { + let fixtureDir; + + /** + * 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); + }); + + 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 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 + }); + + sinon.spy(configArrayFactory, "loadInDirectory"); + + // If cached this should be called only once + getConfig(factory, configPath); + const callcount = configArrayFactory.loadInDirectory.callcount; + + getConfig(factory, configPath); + + 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, + 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, - getEslintAllConfig, - 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, + 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, + useEslintrc: false, getEslintAllConfig, - eslintRecommendedPath + 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, + 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, + useEslintrc: false, getEslintAllConfig, - eslintRecommendedPath + 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, + 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, getEslintAllConfig, - eslintRecommendedPath + 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, + 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, getEslintAllConfig, - eslintRecommendedPath + getEslintRecommendedConfig }); + const file = getFixturePath("root-true", "parent", "root", "wrong-semi.js"); + const expected = { + rules: { + semi: [2, "never"] + }, + ignorePatterns: cwdIgnorePatterns + }; + const actual = getConfig(factory, file); - mockOsHomedir(homePath); - - getConfig(factory, filePath); + assertConfigsEqual(actual, expected); }); - 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"); + // 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({ - cliConfig: { - rules: { quotes: [2, "single"] } - }, - cwd: projectPath, - eslintAllPath, + 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); + 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"); + // 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({ - baseConfig: {}, - cwd: projectPath, + specificConfigPath: getFixturePath("broken", "add-conf.yaml"), getEslintAllConfig, - eslintRecommendedPath + getEslintRecommendedConfig }); + 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); - 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 overrides local .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: getPath(), - eslintAllPath, + specificConfigPath: getFixturePath("broken", "override-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], - semi: [1, "never"] - } + quotes: [0, "double"] + }, + 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 second level .eslintrc + it("should merge command line config when config file adds to local and parent .eslintrc", () => { const factory = new CascadingConfigArrayFactory({ - cwd: getPath(), + specificConfigPath: getFixturePath("broken", "add-conf.yaml"), getEslintAllConfig, - eslintRecommendedPath + getEslintRecommendedConfig }); - const targetPath = getFakeFixturePath("overrides", "child", "child-one.js"); + const file = getFixturePath("broken", "subbroken", "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"], + quotes: [2, "single"], + "no-console": [1], semi: [1, "never"] - } + }, + 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"] - } - }] - }, - useEslintrc: false, - eslintAllPath, - 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, - eslintRecommendedPath - }), /Invalid override pattern/u); - }); - - 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, + 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, + specificConfigPath: getFixturePath("broken", "override-conf.yaml"), getEslintAllConfig, - eslintRecommendedPath + 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, + 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, getEslintAllConfig, - eslintRecommendedPath + 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, + 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, getEslintAllConfig, - eslintRecommendedPath + 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, + 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, - getEslintAllConfig, - 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); + }); }); }); }); @@ -1959,66 +3148,136 @@ 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; - beforeEach(async () => { - await prepare(); - additionalPluginPool = new Map(); - factory = new CascadingConfigArrayFactory({ - cwd: getPath(), - additionalPluginPool, - cliConfig: { plugins: ["test"] }, - eslintAllPath, - getEslintRecommendedConfig + /** @type {Map} */ + let additionalPluginPool; + + /** @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 not have a loading error in CLI config after adding 'test' plugin to the additional plugin pool then calling 'clearCache()'.", () => { + factory.getConfigArrayForFile("a.js"); - it("should have a loading error in CLI config.", () => { - const config = factory.getConfigArrayForFile("a.js"); + additionalPluginPool.set("test", { configs: { name: "test" } }); + factory.clearCache(); - assert.strictEqual(config[2].plugins.test.definition, null); + // 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(); - // Check. - const config = factory.getConfigArrayForFile("a.js"); + /** @type {Map} */ + let additionalPluginPool; - assert.deepStrictEqual( - config[2].plugins.test.definition, - { - configs: { name: "test" }, - environments: {}, - processors: {}, - rules: {} - } - ); + /** @type {CascadingConfigArrayFactory} */ + let factory; + + 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 a0b7e0e2..a87c5059 100644 --- a/tests/lib/config-array-factory.js +++ b/tests/lib/config-array-factory.js @@ -47,7 +47,7 @@ const tempDir = path.join(systemTempDir, "eslintrc/config-array-factory"); * @returns {ConfigData} Config data */ function getEslintAllConfig() { - return import("../fixtures/eslint-all.cjs"); + return require("../fixtures/eslint-all.cjs"); } /** @@ -55,7 +55,7 @@ function getEslintAllConfig() { * @returns {ConfigData} Config data */ function getEslintRecommendedConfig() { - return import("../fixtures/eslint-recommended.cjs"); + return require("../fixtures/eslint-recommended.cjs"); } /** @@ -1043,8 +1043,7 @@ describe("ConfigArrayFactory", () => { 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 + ...getEslintAllConfig() }); }); @@ -1608,217 +1607,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, - getEslintRecommendedConfig + 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] + } + }); }); }); }); @@ -1829,7 +2047,7 @@ describe("ConfigArrayFactory", () => { let cleanup; beforeEach(() => { - cleanup = () => {}; + cleanup = () => { }; }); afterEach(() => cleanup());