From 185982d5615d325ae8b45c2360d5847df4098bda Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sat, 28 Mar 2020 05:11:19 +0900 Subject: [PATCH] Breaking: improve plugin resolving (refs eslint/rfcs#47) (#12922) * Breaking: change relative paths with --config (refs eslint/rfcs#37) * update docs * Breaking: improve plugin resolving (refs eslint/rfcs#47) * replace getRules by getRulesForFile (refs eslint/rfcs#47) * replace rulesMeta by getRuleMeta (refs eslint/rfcs#47) * replace report.usedDeprecatedRules by report.results[].usedDeprecatedRules * Revert "replace report.usedDeprecatedRules by report.results[].usedDeprecatedRules" This reverts commit f3cc32f5c07579aecfc4566201f19918545aca6d. * Revert "replace rulesMeta by getRuleMeta (refs eslint/rfcs#47)" This reverts commit 0d6afaf6a40792c05659796ec4c26258e5143494. * Revert "replace getRules by getRulesForFile (refs eslint/rfcs#47)" This reverts commit d29f613c9d8a8106ca0d68bb324415807dca4a20. * update docs * Update docs/user-guide/configuring.md Co-Authored-By: Kai Cataldo * fix markdownlint error Co-authored-by: Kai Cataldo --- docs/user-guide/configuring.md | 5 +- .../cascading-config-array-factory.js | 2 +- lib/cli-engine/config-array-factory.js | 68 ++-- lib/cli-engine/config-array/config-array.js | 28 ++ messages/plugin-conflict.txt | 7 + .../cascading-config-array-factory.js | 3 +- tests/lib/cli-engine/cli-engine.js | 361 +++++++++++++++++- tests/lib/cli-engine/config-array-factory.js | 14 +- 8 files changed, 448 insertions(+), 40 deletions(-) create mode 100644 messages/plugin-conflict.txt diff --git a/docs/user-guide/configuring.md b/docs/user-guide/configuring.md index 414d0e02524..7f880198bf4 100644 --- a/docs/user-guide/configuring.md +++ b/docs/user-guide/configuring.md @@ -321,7 +321,10 @@ And in YAML: - eslint-plugin-plugin2 ``` -**Note:** Plugins are resolved relative to the current working directory of the ESLint process. In other words, ESLint will load the same plugin as a user would obtain by running `require('eslint-plugin-pluginname')` in a Node REPL from their project root. +**Notes:** + +1. Plugins are resolved relative to the config file. In other words, ESLint will load the plugin as a user would obtain by running `require('eslint-plugin-pluginname')` in the config file. +2. Plugins in the base configuration (loaded by `extends` setting) are relative to the derived config file. For example, if `./.eslintrc` has `extends: ["foo"]` and the `eslint-config-foo` has `plugins: ["bar"]`, ESLint finds the `eslint-plugin-bar` from `./node_modules/` (rather than `./node_modules/eslint-config-foo/node_modules/`) or ancestor directories. Thus every plugin in the config file and base configurations is resolved uniquely. ### Naming Convention diff --git a/lib/cli-engine/cascading-config-array-factory.js b/lib/cli-engine/cascading-config-array-factory.js index 4c362d9000a..b53f67bd9dc 100644 --- a/lib/cli-engine/cascading-config-array-factory.js +++ b/lib/cli-engine/cascading-config-array-factory.js @@ -199,7 +199,7 @@ class CascadingConfigArrayFactory { cliConfig: cliConfigData = null, cwd = process.cwd(), ignorePath, - resolvePluginsRelativeTo = cwd, + resolvePluginsRelativeTo, rulePaths = [], specificConfigPath = null, useEslintrc = true diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js index c5816bf445e..b1429af6ad9 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -90,7 +90,16 @@ const configFilenames = [ * @typedef {Object} ConfigArrayFactoryInternalSlots * @property {Map} additionalPluginPool The map for additional plugins. * @property {string} cwd The path to the current working directory. - * @property {string} resolvePluginsRelativeTo An absolute path the the directory that plugins should be resolved from. + * @property {string | undefined} resolvePluginsRelativeTo An absolute path the the directory that plugins should be resolved from. + */ + +/** + * @typedef {Object} ConfigArrayFactoryLoadingContext + * @property {string} filePath The path to the current configuration. + * @property {string} matchBasePath The base path to resolve relative paths in `overrides[].files`, `overrides[].excludedFiles`, and `ignorePatterns`. + * @property {string} name The name of the current configuration. + * @property {string} pluginBasePath The base path to resolve plugins. + * @property {"config" | "ignore" | "implicit-processor"} type The type of the current configuration. This is `"config"` in normal. This is `"ignore"` if it came from `.eslintignore`. This is `"implicit-processor"` if it came from legacy file-extension processors. */ /** @@ -337,7 +346,7 @@ function writeDebugLogForLoading(request, relativeTo, filePath) { /** * Create a new context with default values. - * @param {string | undefined} cwd The current working directory. + * @param {ConfigArrayFactoryInternalSlots} slots The internal slots. * @param {"config" | "ignore" | "implicit-processor" | undefined} providedType The type of the current configuration. Default is `"config"`. * @param {string | undefined} providedName The name of the current configuration. Default is the relative path from `cwd` to `filePath`. * @param {string | undefined} providedFilePath The path to the current configuration. Default is empty string. @@ -345,7 +354,7 @@ function writeDebugLogForLoading(request, relativeTo, filePath) { * @returns {ConfigArrayFactoryLoadingContext} The created context. */ function createContext( - cwd, + { cwd, resolvePluginsRelativeTo }, providedType, providedName, providedFilePath, @@ -355,16 +364,20 @@ function createContext( ? path.resolve(cwd, providedFilePath) : ""; const matchBasePath = - providedMatchBasePath || + (providedMatchBasePath && path.resolve(cwd, providedMatchBasePath)) || (filePath && path.dirname(filePath)) || cwd; const name = providedName || (filePath && path.relative(cwd, filePath)) || ""; + const pluginBasePath = + resolvePluginsRelativeTo || + (filePath && path.dirname(filePath)) || + cwd; const type = providedType || "config"; - return { filePath, matchBasePath, name, type }; + return { filePath, matchBasePath, name, pluginBasePath, type }; } /** @@ -399,12 +412,14 @@ class ConfigArrayFactory { constructor({ additionalPluginPool = new Map(), cwd = process.cwd(), - resolvePluginsRelativeTo = cwd + resolvePluginsRelativeTo } = {}) { internalSlotsMap.set(this, { additionalPluginPool, cwd, - resolvePluginsRelativeTo: path.resolve(cwd, resolvePluginsRelativeTo) + resolvePluginsRelativeTo: + resolvePluginsRelativeTo && + path.resolve(cwd, resolvePluginsRelativeTo) }); } @@ -422,8 +437,8 @@ class ConfigArrayFactory { return new ConfigArray(); } - const { cwd } = internalSlotsMap.get(this); - const ctx = createContext(cwd, "config", name, filePath, basePath); + const slots = internalSlotsMap.get(this); + const ctx = createContext(slots, "config", name, filePath, basePath); const elements = this._normalizeConfigData(configData, ctx); return new ConfigArray(...elements); @@ -438,8 +453,8 @@ class ConfigArrayFactory { * @returns {ConfigArray} Loaded config. */ loadFile(filePath, { basePath, name } = {}) { - const { cwd } = internalSlotsMap.get(this); - const ctx = createContext(cwd, "config", name, filePath, basePath); + const slots = internalSlotsMap.get(this); + const ctx = createContext(slots, "config", name, filePath, basePath); return new ConfigArray(...this._loadConfigData(ctx)); } @@ -453,11 +468,11 @@ class ConfigArrayFactory { * @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist. */ loadInDirectory(directoryPath, { basePath, name } = {}) { - const { cwd } = internalSlotsMap.get(this); + const slots = internalSlotsMap.get(this); for (const filename of configFilenames) { const ctx = createContext( - cwd, + slots, "config", name, path.join(directoryPath, filename), @@ -517,16 +532,15 @@ class ConfigArrayFactory { * @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist. */ loadESLintIgnore(filePath) { - const { cwd } = internalSlotsMap.get(this); - const absolutePath = path.resolve(cwd, filePath); - const ignorePatterns = loadESLintIgnoreFile(absolutePath); + const slots = internalSlotsMap.get(this); const ctx = createContext( - cwd, + slots, "ignore", void 0, - absolutePath, - cwd + filePath, + slots.cwd ); + const ignorePatterns = loadESLintIgnoreFile(ctx.filePath); return new ConfigArray( ...this._normalizeESLintIgnoreData(ignorePatterns, ctx) @@ -538,9 +552,9 @@ class ConfigArrayFactory { * @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist. */ loadDefaultESLintIgnore() { - const { cwd } = internalSlotsMap.get(this); - const eslintIgnorePath = path.resolve(cwd, ".eslintignore"); - const packageJsonPath = path.resolve(cwd, "package.json"); + const slots = internalSlotsMap.get(this); + const eslintIgnorePath = path.resolve(slots.cwd, ".eslintignore"); + const packageJsonPath = path.resolve(slots.cwd, "package.json"); if (fs.existsSync(eslintIgnorePath)) { return this.loadESLintIgnore(eslintIgnorePath); @@ -553,11 +567,11 @@ class ConfigArrayFactory { throw new Error("Package.json eslintIgnore property requires an array of paths"); } const ctx = createContext( - cwd, + slots, "ignore", "eslintIgnore in package.json", packageJsonPath, - cwd + slots.cwd ); return new ConfigArray( @@ -934,10 +948,10 @@ class ConfigArrayFactory { _loadPlugin(name, ctx) { debug("Loading plugin %j from %s", name, ctx.filePath); - const { additionalPluginPool, resolvePluginsRelativeTo } = internalSlotsMap.get(this); + const { additionalPluginPool } = internalSlotsMap.get(this); const request = naming.normalizePackageName(name, "eslint-plugin"); const id = naming.getShorthandName(request, "eslint-plugin"); - const relativeTo = path.join(resolvePluginsRelativeTo, "__placeholder__.js"); + const relativeTo = path.join(ctx.pluginBasePath, "__placeholder__.js"); if (name.match(/\s+/u)) { const error = Object.assign( @@ -983,7 +997,7 @@ class ConfigArrayFactory { error.messageTemplate = "plugin-missing"; error.messageData = { pluginName: request, - resolvePluginsRelativeTo, + resolvePluginsRelativeTo: ctx.pluginBasePath, importerName: ctx.name }; } diff --git a/lib/cli-engine/config-array/config-array.js b/lib/cli-engine/config-array/config-array.js index 1d10fe1b3f9..b3434198b19 100644 --- a/lib/cli-engine/config-array/config-array.js +++ b/lib/cli-engine/config-array/config-array.js @@ -156,6 +156,23 @@ function mergeWithoutOverwrite(target, source) { } } +/** + * The error for plugin conflicts. + */ +class PluginConflictError extends Error { + + /** + * Initialize this error object. + * @param {string} pluginId The plugin ID. + * @param {{filePath:string, importerName:string}[]} plugins The resolved plugins. + */ + constructor(pluginId, plugins) { + super(`Plugin "${pluginId}" was conflicted between ${plugins.map(p => `"${p.importerName}"`).join(" and ")}.`); + this.messageTemplate = "plugin-conflict"; + this.messageData = { pluginId, plugins }; + } +} + /** * Merge plugins. * `target`'s definition is prior to `source`'s. @@ -181,6 +198,17 @@ function mergePlugins(target, source) { throw sourceValue.error; } target[key] = sourceValue; + } else if (sourceValue.filePath !== targetValue.filePath) { + throw new PluginConflictError(key, [ + { + filePath: targetValue.filePath, + importerName: targetValue.importerName + }, + { + filePath: sourceValue.filePath, + importerName: sourceValue.importerName + } + ]); } } } diff --git a/messages/plugin-conflict.txt b/messages/plugin-conflict.txt new file mode 100644 index 00000000000..6fcf7c83115 --- /dev/null +++ b/messages/plugin-conflict.txt @@ -0,0 +1,7 @@ +ESLint couldn't determine the plugin "<%- pluginId %>" uniquely. +<% for (const { filePath, importerName } of plugins) { %> +- <%= filePath %> (loaded in "<%= importerName %>")<% } %> + +Please remove the "plugins" setting from either config or remove either plugin installation. + +If you still can't figure out the problem, please stop by https://gitter.im/eslint/eslint to chat with the team. diff --git a/tests/lib/cli-engine/cascading-config-array-factory.js b/tests/lib/cli-engine/cascading-config-array-factory.js index 0bc49b461bf..f9817717f72 100644 --- a/tests/lib/cli-engine/cascading-config-array-factory.js +++ b/tests/lib/cli-engine/cascading-config-array-factory.js @@ -938,7 +938,8 @@ describe("CascadingConfigArrayFactory", () => { cliConfig: { plugins: ["another-plugin"] }, - cwd: getFixturePath("plugins") + cwd: getFixturePath("plugins"), + resolvePluginsRelativeTo: getFixturePath("plugins") }); const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"); const expected = { diff --git a/tests/lib/cli-engine/cli-engine.js b/tests/lib/cli-engine/cli-engine.js index ba631f38344..7a5ebf7f9b9 100644 --- a/tests/lib/cli-engine/cli-engine.js +++ b/tests/lib/cli-engine/cli-engine.js @@ -3394,7 +3394,7 @@ describe("CLIEngine", () => { assert.deepStrictEqual(err.messageData, { importerName: `extends-plugin${path.sep}.eslintrc.yml`, pluginName: "eslint-plugin-nonexistent-plugin", - resolvePluginsRelativeTo: cwd + resolvePluginsRelativeTo: path.join(cwd, "extends-plugin") // the directory of the config file. }); return; } @@ -3410,7 +3410,7 @@ describe("CLIEngine", () => { assert.deepStrictEqual(err.messageData, { importerName: `plugins${path.sep}.eslintrc.yml`, pluginName: "eslint-plugin-nonexistent-plugin", - resolvePluginsRelativeTo: cwd + resolvePluginsRelativeTo: path.join(cwd, "plugins") // the directory of the config file. }); return; } @@ -5904,4 +5904,361 @@ describe("CLIEngine", () => { }); }); }); + + describe("plugin conflicts", () => { + let uid = 0; + let root = ""; + + beforeEach(() => { + root = getFixturePath(`cli-engine/plugin-conflicts-${++uid}`); + }); + + /** @type {typeof CLIEngine} */ + let InMemoryCLIEngine; + + /** + * Verify thrown errors. + * @param {() => void} f The function to run and throw. + * @param {Record} props The properties to verify. + * @returns {void} + */ + function assertThrows(f, props) { + try { + f(); + } catch (error) { + for (const [key, value] of Object.entries(props)) { + assert.deepStrictEqual(error[key], value, key); + } + return; + } + + assert.fail("Function should throw an error, but not."); + } + + describe("between a config file and linear extendees.", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + "node_modules/eslint-config-one/node_modules/eslint-plugin-foo/index.js": "", + "node_modules/eslint-config-one/index.js": `module.exports = ${JSON.stringify({ + extends: ["two"], + plugins: ["foo"] + })}`, + "node_modules/eslint-config-two/node_modules/eslint-plugin-foo/index.js": "", + "node_modules/eslint-config-two/index.js": `module.exports = ${JSON.stringify({ + plugins: ["foo"] + })}`, + ".eslintrc.json": JSON.stringify({ + extends: ["one"], + plugins: ["foo"] + }), + "test.js": "" + } + }).CLIEngine; + }); + + it("'executeOnFiles()' should NOT throw plugin-conflict error. (Load the plugin from the base directory of the entry config file.)", () => { + const engine = new InMemoryCLIEngine({ cwd: root }); + + engine.executeOnFiles("test.js"); + }); + }); + + describe("between a config file and same-depth extendees.", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + "node_modules/eslint-config-one/node_modules/eslint-plugin-foo/index.js": "", + "node_modules/eslint-config-one/index.js": `module.exports = ${JSON.stringify({ + plugins: ["foo"] + })}`, + "node_modules/eslint-config-two/node_modules/eslint-plugin-foo/index.js": "", + "node_modules/eslint-config-two/index.js": `module.exports = ${JSON.stringify({ + plugins: ["foo"] + })}`, + ".eslintrc.json": JSON.stringify({ + extends: ["one", "two"], + plugins: ["foo"] + }), + "test.js": "" + } + }).CLIEngine; + }); + + it("'executeOnFiles()' should NOT throw plugin-conflict error. (Load the plugin from the base directory of the entry config file.)", () => { + const engine = new InMemoryCLIEngine({ cwd: root }); + + engine.executeOnFiles("test.js"); + }); + }); + + describe("between two config files in different directories, with single node_modules.", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + ".eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/test.js": "" + } + }).CLIEngine; + }); + + it("'executeOnFiles()' should NOT throw plugin-conflict error. (Load the plugin from the base directory of the entry config file, but there are two entry config files, but node_modules directory is unique.)", () => { + const engine = new InMemoryCLIEngine({ cwd: root }); + + engine.executeOnFiles("subdir/test.js"); + }); + }); + + describe("between two config files in different directories, with multiple node_modules.", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + ".eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/node_modules/eslint-plugin-foo/index.js": "", + "subdir/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/test.js": "" + } + }).CLIEngine; + }); + + it("'executeOnFiles()' should throw plugin-conflict error. (Load the plugin from the base directory of the entry config file, but there are two entry config files.)", () => { + const engine = new InMemoryCLIEngine({ cwd: root }); + + assertThrows( + () => engine.executeOnFiles("subdir/test.js"), + { + message: `Plugin "foo" was conflicted between "subdir${path.sep}.eslintrc.json" and ".eslintrc.json".`, + messageTemplate: "plugin-conflict", + messageData: { + pluginId: "foo", + plugins: [ + { + filePath: path.join(root, "subdir/node_modules/eslint-plugin-foo/index.js"), + importerName: `subdir${path.sep}.eslintrc.json` + }, + { + filePath: path.join(root, "node_modules/eslint-plugin-foo/index.js"), + importerName: ".eslintrc.json" + } + ] + } + } + ); + }); + }); + + describe("between '--config' option and a regular config file, with single node_modules.", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + "node_modules/mine/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + ".eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "test.js": "" + } + }).CLIEngine; + }); + + it("'executeOnFiles()' should NOT throw plugin-conflict error. (Load the plugin from the base directory of the entry config file, but there are two entry config files, but node_modules directory is unique.)", () => { + const engine = new InMemoryCLIEngine({ + cwd: root, + configFile: "node_modules/mine/.eslintrc.json" + }); + + engine.executeOnFiles("test.js"); + }); + }); + + describe("between '--config' option and a regular config file, with multiple node_modules.", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + "node_modules/mine/node_modules/eslint-plugin-foo/index.js": "", + "node_modules/mine/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + ".eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "test.js": "" + } + }).CLIEngine; + }); + + it("'executeOnFiles()' should throw plugin-conflict error. (Load the plugin from the base directory of the entry config file, but there are two entry config files.)", () => { + const engine = new InMemoryCLIEngine({ + cwd: root, + configFile: "node_modules/mine/.eslintrc.json" + }); + + assertThrows( + () => engine.executeOnFiles("test.js"), + { + message: "Plugin \"foo\" was conflicted between \"--config\" and \".eslintrc.json\".", + messageTemplate: "plugin-conflict", + messageData: { + pluginId: "foo", + plugins: [ + { + filePath: path.join(root, "node_modules/mine/node_modules/eslint-plugin-foo/index.js"), + importerName: "--config" + }, + { + filePath: path.join(root, "node_modules/eslint-plugin-foo/index.js"), + importerName: ".eslintrc.json" + } + ] + } + } + ); + }); + }); + + describe("between '--plugin' option and a regular config file, with single node_modules.", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + "subdir/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/test.js": "" + } + }).CLIEngine; + }); + + it("'executeOnFiles()' should NOT throw plugin-conflict error. (Load the plugin from both CWD and the base directory of the entry config file, but node_modules directory is unique.)", () => { + const engine = new InMemoryCLIEngine({ + cwd: root, + plugins: ["foo"] + }); + + engine.executeOnFiles("subdir/test.js"); + }); + }); + + describe("between '--plugin' option and a regular config file, with multiple node_modules.", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + "subdir/node_modules/eslint-plugin-foo/index.js": "", + "subdir/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/test.js": "" + } + }).CLIEngine; + }); + + it("'executeOnFiles()' should throw plugin-conflict error. (Load the plugin from both CWD and the base directory of the entry config file.)", () => { + const engine = new InMemoryCLIEngine({ + cwd: root, + plugins: ["foo"] + }); + + assertThrows( + () => engine.executeOnFiles("subdir/test.js"), + { + message: `Plugin "foo" was conflicted between "CLIOptions" and "subdir${path.sep}.eslintrc.json".`, + messageTemplate: "plugin-conflict", + messageData: { + pluginId: "foo", + plugins: [ + { + filePath: path.join(root, "node_modules/eslint-plugin-foo/index.js"), + importerName: "CLIOptions" + }, + { + filePath: path.join(root, "subdir/node_modules/eslint-plugin-foo/index.js"), + importerName: `subdir${path.sep}.eslintrc.json` + } + ] + } + } + ); + }); + }); + + describe("'--resolve-plugins-relative-to' option overrides the location that ESLint load plugins from.", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + ".eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/node_modules/eslint-plugin-foo/index.js": "", + "subdir/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/test.js": "" + } + }).CLIEngine; + }); + + it("'executeOnFiles()' should NOT throw plugin-conflict error. (Load the plugin from '--resolve-plugins-relative-to'.)", () => { + const engine = new InMemoryCLIEngine({ + cwd: root, + resolvePluginsRelativeTo: root + }); + + engine.executeOnFiles("subdir/test.js"); + }); + }); + + describe("between two config files with different target files.", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + "one/node_modules/eslint-plugin-foo/index.js": "", + "one/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "one/test.js": "", + "two/node_modules/eslint-plugin-foo/index.js": "", + "two/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "two/test.js": "" + } + }).CLIEngine; + }); + + it("'executeOnFiles()' should NOT throw plugin-conflict error. (Load the plugin from the base directory of the entry config file for each target file. Not related to each other.)", () => { + const engine = new InMemoryCLIEngine({ cwd: root }); + const { results } = engine.executeOnFiles("*/test.js"); + + assert.strictEqual(results.length, 2); + }); + }); + }); }); diff --git a/tests/lib/cli-engine/config-array-factory.js b/tests/lib/cli-engine/config-array-factory.js index e3e7759de21..20f256bbe61 100644 --- a/tests/lib/cli-engine/config-array-factory.js +++ b/tests/lib/cli-engine/config-array-factory.js @@ -125,7 +125,7 @@ describe("ConfigArrayFactory", () => { assert.strictEqual(normalizeConfigData.callCount, 1); assert.deepStrictEqual(normalizeConfigData.args[0], [ configData, - createContext(tempDir, void 0, name, filePath, basePath) + createContext({ cwd: tempDir }, void 0, name, filePath, basePath) ]); }); @@ -215,7 +215,7 @@ describe("ConfigArrayFactory", () => { assert.strictEqual(normalizeConfigData.callCount, 1); assert.deepStrictEqual(normalizeConfigData.args[0], [ { settings: { name: filePath } }, - createContext(tempDir, void 0, name, filePath, basePath) + createContext({ cwd: tempDir }, void 0, name, filePath, basePath) ]); }); @@ -303,7 +303,7 @@ describe("ConfigArrayFactory", () => { assert.strictEqual(normalizeConfigData.callCount, 1); assert.deepStrictEqual(normalizeConfigData.args[0], [ { settings: { name: `${directoryPath}/.eslintrc.js` } }, - createContext(tempDir, void 0, name, path.join(directoryPath, ".eslintrc.js"), basePath) + createContext({ cwd: tempDir }, void 0, name, path.join(directoryPath, ".eslintrc.js"), basePath) ]); }); @@ -338,7 +338,7 @@ describe("ConfigArrayFactory", () => { * @returns {ConfigArray} The created config array. */ function create(configData, { filePath, name } = {}) { - const ctx = createContext(tempDir, void 0, name, filePath, void 0); + const ctx = createContext({ cwd: tempDir }, void 0, name, filePath, void 0); return new ConfigArray(...factory._normalizeConfigData(configData, ctx)); // eslint-disable-line no-underscore-dangle } @@ -682,7 +682,7 @@ describe("ConfigArrayFactory", () => { }); describe("even if 'plugins' property was given and 'filePath' option was given,", () => { - it("should load the plugin from the project root.", () => { + it("should load the plugin from 'subdir'.", () => { const configArray = create( { plugins: ["subdir"] }, { filePath: path.resolve(tempDir, "subdir/a.js") } @@ -690,9 +690,7 @@ describe("ConfigArrayFactory", () => { assert.strictEqual( configArray[0].plugins.subdir.filePath, - - // "subdir/node_modules/eslint-plugin-subdir/index.js" exists, but not it. - path.resolve(tempDir, "node_modules/eslint-plugin-subdir/index.js") + path.resolve(tempDir, "subdir/node_modules/eslint-plugin-subdir/index.js") ); }); });