Skip to content

Commit

Permalink
Breaking: improve plugin resolving (refs eslint/rfcs#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
mysticatea committed Feb 11, 2020
1 parent a30f44b commit 3f09215
Show file tree
Hide file tree
Showing 7 changed files with 436 additions and 39 deletions.
2 changes: 1 addition & 1 deletion lib/cli-engine/cascading-config-array-factory.js
Expand Up @@ -199,7 +199,7 @@ class CascadingConfigArrayFactory {
cliConfig: cliConfigData = null,
cwd = process.cwd(),
ignorePath,
resolvePluginsRelativeTo = cwd,
resolvePluginsRelativeTo,
rulePaths = [],
specificConfigPath = null,
useEslintrc = true
Expand Down
60 changes: 33 additions & 27 deletions lib/cli-engine/config-array-factory.js
Expand Up @@ -90,14 +90,15 @@ const configFilenames = [
* @typedef {Object} ConfigArrayFactoryInternalSlots
* @property {Map<string,Plugin>} 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.
*/

Expand Down Expand Up @@ -337,15 +338,15 @@ 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.
* @param {string | undefined} providedMatchBasePath The type of the current configuration. Default is the directory of `filePath` or `cwd`.
* @returns {ConfigArrayFactoryLoadingContext} The created context.
*/
function createContext(
cwd,
{ cwd, resolvePluginsRelativeTo },
providedType,
providedName,
providedFilePath,
Expand All @@ -355,16 +356,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 };
}

/**
Expand Down Expand Up @@ -399,12 +404,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)
});
}

Expand All @@ -422,8 +429,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);
Expand All @@ -438,8 +445,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));
}
Expand All @@ -453,11 +460,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),
Expand Down Expand Up @@ -517,16 +524,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)
Expand All @@ -538,9 +544,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);
Expand All @@ -553,11 +559,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(
Expand Down Expand Up @@ -934,10 +940,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(
Expand Down Expand Up @@ -983,7 +989,7 @@ class ConfigArrayFactory {
error.messageTemplate = "plugin-missing";
error.messageData = {
pluginName: request,
resolvePluginsRelativeTo,
resolvePluginsRelativeTo: ctx.pluginBasePath,
importerName: ctx.name
};
}
Expand Down
28 changes: 28 additions & 0 deletions lib/cli-engine/config-array/config-array.js
Expand Up @@ -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.
Expand All @@ -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
}
]);
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions 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.
3 changes: 2 additions & 1 deletion tests/lib/cli-engine/cascading-config-array-factory.js
Expand Up @@ -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 = {
Expand Down

0 comments on commit 3f09215

Please sign in to comment.