4295 lines
143 KiB
JavaScript
4295 lines
143 KiB
JavaScript
'use strict';
|
|
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
|
var debugOrig = require('debug');
|
|
var fs = require('fs');
|
|
var importFresh = require('import-fresh');
|
|
var Module = require('module');
|
|
var path = require('path');
|
|
var stripComments = require('strip-json-comments');
|
|
var assert = require('assert');
|
|
var ignore = require('ignore');
|
|
var util = require('util');
|
|
var minimatch = require('minimatch');
|
|
var Ajv = require('ajv');
|
|
var globals = require('globals');
|
|
var os = require('os');
|
|
|
|
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
|
|
var debugOrig__default = /*#__PURE__*/_interopDefaultLegacy(debugOrig);
|
|
var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
|
|
var importFresh__default = /*#__PURE__*/_interopDefaultLegacy(importFresh);
|
|
var Module__default = /*#__PURE__*/_interopDefaultLegacy(Module);
|
|
var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
|
|
var stripComments__default = /*#__PURE__*/_interopDefaultLegacy(stripComments);
|
|
var assert__default = /*#__PURE__*/_interopDefaultLegacy(assert);
|
|
var ignore__default = /*#__PURE__*/_interopDefaultLegacy(ignore);
|
|
var util__default = /*#__PURE__*/_interopDefaultLegacy(util);
|
|
var minimatch__default = /*#__PURE__*/_interopDefaultLegacy(minimatch);
|
|
var Ajv__default = /*#__PURE__*/_interopDefaultLegacy(Ajv);
|
|
var globals__default = /*#__PURE__*/_interopDefaultLegacy(globals);
|
|
var os__default = /*#__PURE__*/_interopDefaultLegacy(os);
|
|
|
|
/**
|
|
* @fileoverview `IgnorePattern` class.
|
|
*
|
|
* `IgnorePattern` class has the set of glob patterns and the base path.
|
|
*
|
|
* It provides two static methods.
|
|
*
|
|
* - `IgnorePattern.createDefaultIgnore(cwd)`
|
|
* Create the default predicate function.
|
|
* - `IgnorePattern.createIgnore(ignorePatterns)`
|
|
* Create the predicate function from multiple `IgnorePattern` objects.
|
|
*
|
|
* It provides two properties and a method.
|
|
*
|
|
* - `patterns`
|
|
* The glob patterns that ignore to lint.
|
|
* - `basePath`
|
|
* The base path of the glob patterns. If absolute paths existed in the
|
|
* glob patterns, those are handled as relative paths to the base path.
|
|
* - `getPatternsRelativeTo(basePath)`
|
|
* Get `patterns` as modified for a given base path. It modifies the
|
|
* absolute paths in the patterns as prepending the difference of two base
|
|
* paths.
|
|
*
|
|
* `ConfigArrayFactory` creates `IgnorePattern` objects when it processes
|
|
* `ignorePatterns` properties.
|
|
*
|
|
* @author Toru Nagashima <https://github.com/mysticatea>
|
|
*/
|
|
|
|
const debug$3 = debugOrig__default["default"]("eslintrc:ignore-pattern");
|
|
|
|
/** @typedef {ReturnType<import("ignore").default>} Ignore */
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Helpers
|
|
//------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Get the path to the common ancestor directory of given paths.
|
|
* @param {string[]} sourcePaths The paths to calculate the common ancestor.
|
|
* @returns {string} The path to the common ancestor directory.
|
|
*/
|
|
function getCommonAncestorPath(sourcePaths) {
|
|
let result = sourcePaths[0];
|
|
|
|
for (let i = 1; i < sourcePaths.length; ++i) {
|
|
const a = result;
|
|
const b = sourcePaths[i];
|
|
|
|
// Set the shorter one (it's the common ancestor if one includes the other).
|
|
result = a.length < b.length ? a : b;
|
|
|
|
// Set the common ancestor.
|
|
for (let j = 0, lastSepPos = 0; j < a.length && j < b.length; ++j) {
|
|
if (a[j] !== b[j]) {
|
|
result = a.slice(0, lastSepPos);
|
|
break;
|
|
}
|
|
if (a[j] === path__default["default"].sep) {
|
|
lastSepPos = j;
|
|
}
|
|
}
|
|
}
|
|
|
|
let resolvedResult = result || path__default["default"].sep;
|
|
|
|
// if Windows common ancestor is root of drive must have trailing slash to be absolute.
|
|
if (resolvedResult && resolvedResult.endsWith(":") && process.platform === "win32") {
|
|
resolvedResult += path__default["default"].sep;
|
|
}
|
|
return resolvedResult;
|
|
}
|
|
|
|
/**
|
|
* Make relative path.
|
|
* @param {string} from The source path to get relative path.
|
|
* @param {string} to The destination path to get relative path.
|
|
* @returns {string} The relative path.
|
|
*/
|
|
function relative(from, to) {
|
|
const relPath = path__default["default"].relative(from, to);
|
|
|
|
if (path__default["default"].sep === "/") {
|
|
return relPath;
|
|
}
|
|
return relPath.split(path__default["default"].sep).join("/");
|
|
}
|
|
|
|
/**
|
|
* Get the trailing slash if existed.
|
|
* @param {string} filePath The path to check.
|
|
* @returns {string} The trailing slash if existed.
|
|
*/
|
|
function dirSuffix(filePath) {
|
|
const isDir = (
|
|
filePath.endsWith(path__default["default"].sep) ||
|
|
(process.platform === "win32" && filePath.endsWith("/"))
|
|
);
|
|
|
|
return isDir ? "/" : "";
|
|
}
|
|
|
|
const DefaultPatterns = Object.freeze(["/**/node_modules/*"]);
|
|
const DotPatterns = Object.freeze([".*", "!.eslintrc.*", "!../"]);
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Public
|
|
//------------------------------------------------------------------------------
|
|
|
|
class IgnorePattern {
|
|
|
|
/**
|
|
* The default patterns.
|
|
* @type {string[]}
|
|
*/
|
|
static get DefaultPatterns() {
|
|
return DefaultPatterns;
|
|
}
|
|
|
|
/**
|
|
* Create the default predicate function.
|
|
* @param {string} cwd The current working directory.
|
|
* @returns {((filePath:string, dot:boolean) => boolean) & {basePath:string; patterns:string[]}}
|
|
* The preficate function.
|
|
* The first argument is an absolute path that is checked.
|
|
* The second argument is the flag to not ignore dotfiles.
|
|
* If the predicate function returned `true`, it means the path should be ignored.
|
|
*/
|
|
static createDefaultIgnore(cwd) {
|
|
return this.createIgnore([new IgnorePattern(DefaultPatterns, cwd)]);
|
|
}
|
|
|
|
/**
|
|
* Create the predicate function from multiple `IgnorePattern` objects.
|
|
* @param {IgnorePattern[]} ignorePatterns The list of ignore patterns.
|
|
* @returns {((filePath:string, dot?:boolean) => boolean) & {basePath:string; patterns:string[]}}
|
|
* The preficate function.
|
|
* The first argument is an absolute path that is checked.
|
|
* The second argument is the flag to not ignore dotfiles.
|
|
* If the predicate function returned `true`, it means the path should be ignored.
|
|
*/
|
|
static createIgnore(ignorePatterns) {
|
|
debug$3("Create with: %o", ignorePatterns);
|
|
|
|
const basePath = getCommonAncestorPath(ignorePatterns.map(p => p.basePath));
|
|
const patterns = [].concat(
|
|
...ignorePatterns.map(p => p.getPatternsRelativeTo(basePath))
|
|
);
|
|
const ig = ignore__default["default"]({ allowRelativePaths: true }).add([...DotPatterns, ...patterns]);
|
|
const dotIg = ignore__default["default"]({ allowRelativePaths: true }).add(patterns);
|
|
|
|
debug$3(" processed: %o", { basePath, patterns });
|
|
|
|
return Object.assign(
|
|
(filePath, dot = false) => {
|
|
assert__default["default"](path__default["default"].isAbsolute(filePath), "'filePath' should be an absolute path.");
|
|
const relPathRaw = relative(basePath, filePath);
|
|
const relPath = relPathRaw && (relPathRaw + dirSuffix(filePath));
|
|
const adoptedIg = dot ? dotIg : ig;
|
|
const result = relPath !== "" && adoptedIg.ignores(relPath);
|
|
|
|
debug$3("Check", { filePath, dot, relativePath: relPath, result });
|
|
return result;
|
|
},
|
|
{ basePath, patterns }
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Initialize a new `IgnorePattern` instance.
|
|
* @param {string[]} patterns The glob patterns that ignore to lint.
|
|
* @param {string} basePath The base path of `patterns`.
|
|
*/
|
|
constructor(patterns, basePath) {
|
|
assert__default["default"](path__default["default"].isAbsolute(basePath), "'basePath' should be an absolute path.");
|
|
|
|
/**
|
|
* The glob patterns that ignore to lint.
|
|
* @type {string[]}
|
|
*/
|
|
this.patterns = patterns;
|
|
|
|
/**
|
|
* The base path of `patterns`.
|
|
* @type {string}
|
|
*/
|
|
this.basePath = basePath;
|
|
|
|
/**
|
|
* If `true` then patterns which don't start with `/` will match the paths to the outside of `basePath`. Defaults to `false`.
|
|
*
|
|
* It's set `true` for `.eslintignore`, `package.json`, and `--ignore-path` for backward compatibility.
|
|
* It's `false` as-is for `ignorePatterns` property in config files.
|
|
* @type {boolean}
|
|
*/
|
|
this.loose = false;
|
|
}
|
|
|
|
/**
|
|
* Get `patterns` as modified for a given base path. It modifies the
|
|
* absolute paths in the patterns as prepending the difference of two base
|
|
* paths.
|
|
* @param {string} newBasePath The base path.
|
|
* @returns {string[]} Modifired patterns.
|
|
*/
|
|
getPatternsRelativeTo(newBasePath) {
|
|
assert__default["default"](path__default["default"].isAbsolute(newBasePath), "'newBasePath' should be an absolute path.");
|
|
const { basePath, loose, patterns } = this;
|
|
|
|
if (newBasePath === basePath) {
|
|
return patterns;
|
|
}
|
|
const prefix = `/${relative(newBasePath, basePath)}`;
|
|
|
|
return patterns.map(pattern => {
|
|
const negative = pattern.startsWith("!");
|
|
const head = negative ? "!" : "";
|
|
const body = negative ? pattern.slice(1) : pattern;
|
|
|
|
if (body.startsWith("/") || body.startsWith("../")) {
|
|
return `${head}${prefix}${body}`;
|
|
}
|
|
return loose ? pattern : `${head}${prefix}/**/${body}`;
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @fileoverview `ExtractedConfig` class.
|
|
*
|
|
* `ExtractedConfig` class expresses a final configuration for a specific file.
|
|
*
|
|
* It provides one method.
|
|
*
|
|
* - `toCompatibleObjectAsConfigFileContent()`
|
|
* Convert this configuration to the compatible object as the content of
|
|
* config files. It converts the loaded parser and plugins to strings.
|
|
* `CLIEngine#getConfigForFile(filePath)` method uses this method.
|
|
*
|
|
* `ConfigArray#extractConfig(filePath)` creates a `ExtractedConfig` instance.
|
|
*
|
|
* @author Toru Nagashima <https://github.com/mysticatea>
|
|
*/
|
|
|
|
// For VSCode intellisense
|
|
/** @typedef {import("../../shared/types").ConfigData} ConfigData */
|
|
/** @typedef {import("../../shared/types").GlobalConf} GlobalConf */
|
|
/** @typedef {import("../../shared/types").SeverityConf} SeverityConf */
|
|
/** @typedef {import("./config-dependency").DependentParser} DependentParser */
|
|
/** @typedef {import("./config-dependency").DependentPlugin} DependentPlugin */
|
|
|
|
/**
|
|
* Check if `xs` starts with `ys`.
|
|
* @template T
|
|
* @param {T[]} xs The array to check.
|
|
* @param {T[]} ys The array that may be the first part of `xs`.
|
|
* @returns {boolean} `true` if `xs` starts with `ys`.
|
|
*/
|
|
function startsWith(xs, ys) {
|
|
return xs.length >= ys.length && ys.every((y, i) => y === xs[i]);
|
|
}
|
|
|
|
/**
|
|
* The class for extracted config data.
|
|
*/
|
|
class ExtractedConfig {
|
|
constructor() {
|
|
|
|
/**
|
|
* The config name what `noInlineConfig` setting came from.
|
|
* @type {string}
|
|
*/
|
|
this.configNameOfNoInlineConfig = "";
|
|
|
|
/**
|
|
* Environments.
|
|
* @type {Record<string, boolean>}
|
|
*/
|
|
this.env = {};
|
|
|
|
/**
|
|
* Global variables.
|
|
* @type {Record<string, GlobalConf>}
|
|
*/
|
|
this.globals = {};
|
|
|
|
/**
|
|
* The glob patterns that ignore to lint.
|
|
* @type {(((filePath:string, dot?:boolean) => boolean) & { basePath:string; patterns:string[] }) | undefined}
|
|
*/
|
|
this.ignores = void 0;
|
|
|
|
/**
|
|
* The flag that disables directive comments.
|
|
* @type {boolean|undefined}
|
|
*/
|
|
this.noInlineConfig = void 0;
|
|
|
|
/**
|
|
* Parser definition.
|
|
* @type {DependentParser|null}
|
|
*/
|
|
this.parser = null;
|
|
|
|
/**
|
|
* Options for the parser.
|
|
* @type {Object}
|
|
*/
|
|
this.parserOptions = {};
|
|
|
|
/**
|
|
* Plugin definitions.
|
|
* @type {Record<string, DependentPlugin>}
|
|
*/
|
|
this.plugins = {};
|
|
|
|
/**
|
|
* Processor ID.
|
|
* @type {string|null}
|
|
*/
|
|
this.processor = null;
|
|
|
|
/**
|
|
* The flag that reports unused `eslint-disable` directive comments.
|
|
* @type {boolean|undefined}
|
|
*/
|
|
this.reportUnusedDisableDirectives = void 0;
|
|
|
|
/**
|
|
* Rule settings.
|
|
* @type {Record<string, [SeverityConf, ...any[]]>}
|
|
*/
|
|
this.rules = {};
|
|
|
|
/**
|
|
* Shared settings.
|
|
* @type {Object}
|
|
*/
|
|
this.settings = {};
|
|
}
|
|
|
|
/**
|
|
* Convert this config to the compatible object as a config file content.
|
|
* @returns {ConfigData} The converted object.
|
|
*/
|
|
toCompatibleObjectAsConfigFileContent() {
|
|
const {
|
|
/* eslint-disable no-unused-vars */
|
|
configNameOfNoInlineConfig: _ignore1,
|
|
processor: _ignore2,
|
|
/* eslint-enable no-unused-vars */
|
|
ignores,
|
|
...config
|
|
} = this;
|
|
|
|
config.parser = config.parser && config.parser.filePath;
|
|
config.plugins = Object.keys(config.plugins).filter(Boolean).reverse();
|
|
config.ignorePatterns = ignores ? ignores.patterns : [];
|
|
|
|
// Strip the default patterns from `ignorePatterns`.
|
|
if (startsWith(config.ignorePatterns, IgnorePattern.DefaultPatterns)) {
|
|
config.ignorePatterns =
|
|
config.ignorePatterns.slice(IgnorePattern.DefaultPatterns.length);
|
|
}
|
|
|
|
return config;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @fileoverview `ConfigArray` class.
|
|
*
|
|
* `ConfigArray` class expresses the full of a configuration. It has the entry
|
|
* config file, base config files that were extended, loaded parsers, and loaded
|
|
* plugins.
|
|
*
|
|
* `ConfigArray` class provides three properties and two methods.
|
|
*
|
|
* - `pluginEnvironments`
|
|
* - `pluginProcessors`
|
|
* - `pluginRules`
|
|
* The `Map` objects that contain the members of all plugins that this
|
|
* config array contains. Those map objects don't have mutation methods.
|
|
* Those keys are the member ID such as `pluginId/memberName`.
|
|
* - `isRoot()`
|
|
* If `true` then this configuration has `root:true` property.
|
|
* - `extractConfig(filePath)`
|
|
* Extract the final configuration for a given file. This means merging
|
|
* every config array element which that `criteria` property matched. The
|
|
* `filePath` argument must be an absolute path.
|
|
*
|
|
* `ConfigArrayFactory` provides the loading logic of config files.
|
|
*
|
|
* @author Toru Nagashima <https://github.com/mysticatea>
|
|
*/
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Helpers
|
|
//------------------------------------------------------------------------------
|
|
|
|
// Define types for VSCode IntelliSense.
|
|
/** @typedef {import("../../shared/types").Environment} Environment */
|
|
/** @typedef {import("../../shared/types").GlobalConf} GlobalConf */
|
|
/** @typedef {import("../../shared/types").RuleConf} RuleConf */
|
|
/** @typedef {import("../../shared/types").Rule} Rule */
|
|
/** @typedef {import("../../shared/types").Plugin} Plugin */
|
|
/** @typedef {import("../../shared/types").Processor} Processor */
|
|
/** @typedef {import("./config-dependency").DependentParser} DependentParser */
|
|
/** @typedef {import("./config-dependency").DependentPlugin} DependentPlugin */
|
|
/** @typedef {import("./override-tester")["OverrideTester"]} OverrideTester */
|
|
|
|
/**
|
|
* @typedef {Object} ConfigArrayElement
|
|
* @property {string} name The name of this config element.
|
|
* @property {string} filePath The path to the source file of this config element.
|
|
* @property {InstanceType<OverrideTester>|null} criteria The tester for the `files` and `excludedFiles` of this config element.
|
|
* @property {Record<string, boolean>|undefined} env The environment settings.
|
|
* @property {Record<string, GlobalConf>|undefined} globals The global variable settings.
|
|
* @property {IgnorePattern|undefined} ignorePattern The ignore patterns.
|
|
* @property {boolean|undefined} noInlineConfig The flag that disables directive comments.
|
|
* @property {DependentParser|undefined} parser The parser loader.
|
|
* @property {Object|undefined} parserOptions The parser options.
|
|
* @property {Record<string, DependentPlugin>|undefined} plugins The plugin loaders.
|
|
* @property {string|undefined} processor The processor name to refer plugin's processor.
|
|
* @property {boolean|undefined} reportUnusedDisableDirectives The flag to report unused `eslint-disable` comments.
|
|
* @property {boolean|undefined} root The flag to express root.
|
|
* @property {Record<string, RuleConf>|undefined} rules The rule settings
|
|
* @property {Object|undefined} settings The shared settings.
|
|
* @property {"config" | "ignore" | "implicit-processor"} type The element type.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} ConfigArrayInternalSlots
|
|
* @property {Map<string, ExtractedConfig>} cache The cache to extract configs.
|
|
* @property {ReadonlyMap<string, Environment>|null} envMap The map from environment ID to environment definition.
|
|
* @property {ReadonlyMap<string, Processor>|null} processorMap The map from processor ID to environment definition.
|
|
* @property {ReadonlyMap<string, Rule>|null} ruleMap The map from rule ID to rule definition.
|
|
*/
|
|
|
|
/** @type {WeakMap<ConfigArray, ConfigArrayInternalSlots>} */
|
|
const internalSlotsMap$2 = new class extends WeakMap {
|
|
get(key) {
|
|
let value = super.get(key);
|
|
|
|
if (!value) {
|
|
value = {
|
|
cache: new Map(),
|
|
envMap: null,
|
|
processorMap: null,
|
|
ruleMap: null
|
|
};
|
|
super.set(key, value);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
}();
|
|
|
|
/**
|
|
* Get the indices which are matched to a given file.
|
|
* @param {ConfigArrayElement[]} elements The elements.
|
|
* @param {string} filePath The path to a target file.
|
|
* @returns {number[]} The indices.
|
|
*/
|
|
function getMatchedIndices(elements, filePath) {
|
|
const indices = [];
|
|
|
|
for (let i = elements.length - 1; i >= 0; --i) {
|
|
const element = elements[i];
|
|
|
|
if (!element.criteria || (filePath && element.criteria.test(filePath))) {
|
|
indices.push(i);
|
|
}
|
|
}
|
|
|
|
return indices;
|
|
}
|
|
|
|
/**
|
|
* Check if a value is a non-null object.
|
|
* @param {any} x The value to check.
|
|
* @returns {boolean} `true` if the value is a non-null object.
|
|
*/
|
|
function isNonNullObject(x) {
|
|
return typeof x === "object" && x !== null;
|
|
}
|
|
|
|
/**
|
|
* Merge two objects.
|
|
*
|
|
* Assign every property values of `y` to `x` if `x` doesn't have the property.
|
|
* If `x`'s property value is an object, it does recursive.
|
|
* @param {Object} target The destination to merge
|
|
* @param {Object|undefined} source The source to merge.
|
|
* @returns {void}
|
|
*/
|
|
function mergeWithoutOverwrite(target, source) {
|
|
if (!isNonNullObject(source)) {
|
|
return;
|
|
}
|
|
|
|
for (const key of Object.keys(source)) {
|
|
if (key === "__proto__") {
|
|
continue;
|
|
}
|
|
|
|
if (isNonNullObject(target[key])) {
|
|
mergeWithoutOverwrite(target[key], source[key]);
|
|
} else if (target[key] === void 0) {
|
|
if (isNonNullObject(source[key])) {
|
|
target[key] = Array.isArray(source[key]) ? [] : {};
|
|
mergeWithoutOverwrite(target[key], source[key]);
|
|
} else if (source[key] !== void 0) {
|
|
target[key] = source[key];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* @param {Record<string, DependentPlugin>} target The destination to merge
|
|
* @param {Record<string, DependentPlugin>|undefined} source The source to merge.
|
|
* @returns {void}
|
|
*/
|
|
function mergePlugins(target, source) {
|
|
if (!isNonNullObject(source)) {
|
|
return;
|
|
}
|
|
|
|
for (const key of Object.keys(source)) {
|
|
if (key === "__proto__") {
|
|
continue;
|
|
}
|
|
const targetValue = target[key];
|
|
const sourceValue = source[key];
|
|
|
|
// Adopt the plugin which was found at first.
|
|
if (targetValue === void 0) {
|
|
if (sourceValue.error) {
|
|
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
|
|
}
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Merge rule configs.
|
|
* `target`'s definition is prior to `source`'s.
|
|
* @param {Record<string, Array>} target The destination to merge
|
|
* @param {Record<string, RuleConf>|undefined} source The source to merge.
|
|
* @returns {void}
|
|
*/
|
|
function mergeRuleConfigs(target, source) {
|
|
if (!isNonNullObject(source)) {
|
|
return;
|
|
}
|
|
|
|
for (const key of Object.keys(source)) {
|
|
if (key === "__proto__") {
|
|
continue;
|
|
}
|
|
const targetDef = target[key];
|
|
const sourceDef = source[key];
|
|
|
|
// Adopt the rule config which was found at first.
|
|
if (targetDef === void 0) {
|
|
if (Array.isArray(sourceDef)) {
|
|
target[key] = [...sourceDef];
|
|
} else {
|
|
target[key] = [sourceDef];
|
|
}
|
|
|
|
/*
|
|
* If the first found rule config is severity only and the current rule
|
|
* config has options, merge the severity and the options.
|
|
*/
|
|
} else if (
|
|
targetDef.length === 1 &&
|
|
Array.isArray(sourceDef) &&
|
|
sourceDef.length >= 2
|
|
) {
|
|
targetDef.push(...sourceDef.slice(1));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create the extracted config.
|
|
* @param {ConfigArray} instance The config elements.
|
|
* @param {number[]} indices The indices to use.
|
|
* @returns {ExtractedConfig} The extracted config.
|
|
*/
|
|
function createConfig(instance, indices) {
|
|
const config = new ExtractedConfig();
|
|
const ignorePatterns = [];
|
|
|
|
// Merge elements.
|
|
for (const index of indices) {
|
|
const element = instance[index];
|
|
|
|
// Adopt the parser which was found at first.
|
|
if (!config.parser && element.parser) {
|
|
if (element.parser.error) {
|
|
throw element.parser.error;
|
|
}
|
|
config.parser = element.parser;
|
|
}
|
|
|
|
// Adopt the processor which was found at first.
|
|
if (!config.processor && element.processor) {
|
|
config.processor = element.processor;
|
|
}
|
|
|
|
// Adopt the noInlineConfig which was found at first.
|
|
if (config.noInlineConfig === void 0 && element.noInlineConfig !== void 0) {
|
|
config.noInlineConfig = element.noInlineConfig;
|
|
config.configNameOfNoInlineConfig = element.name;
|
|
}
|
|
|
|
// Adopt the reportUnusedDisableDirectives which was found at first.
|
|
if (config.reportUnusedDisableDirectives === void 0 && element.reportUnusedDisableDirectives !== void 0) {
|
|
config.reportUnusedDisableDirectives = element.reportUnusedDisableDirectives;
|
|
}
|
|
|
|
// Collect ignorePatterns
|
|
if (element.ignorePattern) {
|
|
ignorePatterns.push(element.ignorePattern);
|
|
}
|
|
|
|
// Merge others.
|
|
mergeWithoutOverwrite(config.env, element.env);
|
|
mergeWithoutOverwrite(config.globals, element.globals);
|
|
mergeWithoutOverwrite(config.parserOptions, element.parserOptions);
|
|
mergeWithoutOverwrite(config.settings, element.settings);
|
|
mergePlugins(config.plugins, element.plugins);
|
|
mergeRuleConfigs(config.rules, element.rules);
|
|
}
|
|
|
|
// Create the predicate function for ignore patterns.
|
|
if (ignorePatterns.length > 0) {
|
|
config.ignores = IgnorePattern.createIgnore(ignorePatterns.reverse());
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* Collect definitions.
|
|
* @template T, U
|
|
* @param {string} pluginId The plugin ID for prefix.
|
|
* @param {Record<string,T>} defs The definitions to collect.
|
|
* @param {Map<string, U>} map The map to output.
|
|
* @param {function(T): U} [normalize] The normalize function for each value.
|
|
* @returns {void}
|
|
*/
|
|
function collect(pluginId, defs, map, normalize) {
|
|
if (defs) {
|
|
const prefix = pluginId && `${pluginId}/`;
|
|
|
|
for (const [key, value] of Object.entries(defs)) {
|
|
map.set(
|
|
`${prefix}${key}`,
|
|
normalize ? normalize(value) : value
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Normalize a rule definition.
|
|
* @param {Function|Rule} rule The rule definition to normalize.
|
|
* @returns {Rule} The normalized rule definition.
|
|
*/
|
|
function normalizePluginRule(rule) {
|
|
return typeof rule === "function" ? { create: rule } : rule;
|
|
}
|
|
|
|
/**
|
|
* Delete the mutation methods from a given map.
|
|
* @param {Map<any, any>} map The map object to delete.
|
|
* @returns {void}
|
|
*/
|
|
function deleteMutationMethods(map) {
|
|
Object.defineProperties(map, {
|
|
clear: { configurable: true, value: void 0 },
|
|
delete: { configurable: true, value: void 0 },
|
|
set: { configurable: true, value: void 0 }
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create `envMap`, `processorMap`, `ruleMap` with the plugins in the config array.
|
|
* @param {ConfigArrayElement[]} elements The config elements.
|
|
* @param {ConfigArrayInternalSlots} slots The internal slots.
|
|
* @returns {void}
|
|
*/
|
|
function initPluginMemberMaps(elements, slots) {
|
|
const processed = new Set();
|
|
|
|
slots.envMap = new Map();
|
|
slots.processorMap = new Map();
|
|
slots.ruleMap = new Map();
|
|
|
|
for (const element of elements) {
|
|
if (!element.plugins) {
|
|
continue;
|
|
}
|
|
|
|
for (const [pluginId, value] of Object.entries(element.plugins)) {
|
|
const plugin = value.definition;
|
|
|
|
if (!plugin || processed.has(pluginId)) {
|
|
continue;
|
|
}
|
|
processed.add(pluginId);
|
|
|
|
collect(pluginId, plugin.environments, slots.envMap);
|
|
collect(pluginId, plugin.processors, slots.processorMap);
|
|
collect(pluginId, plugin.rules, slots.ruleMap, normalizePluginRule);
|
|
}
|
|
}
|
|
|
|
deleteMutationMethods(slots.envMap);
|
|
deleteMutationMethods(slots.processorMap);
|
|
deleteMutationMethods(slots.ruleMap);
|
|
}
|
|
|
|
/**
|
|
* Create `envMap`, `processorMap`, `ruleMap` with the plugins in the config array.
|
|
* @param {ConfigArray} instance The config elements.
|
|
* @returns {ConfigArrayInternalSlots} The extracted config.
|
|
*/
|
|
function ensurePluginMemberMaps(instance) {
|
|
const slots = internalSlotsMap$2.get(instance);
|
|
|
|
if (!slots.ruleMap) {
|
|
initPluginMemberMaps(instance, slots);
|
|
}
|
|
|
|
return slots;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Public Interface
|
|
//------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* The Config Array.
|
|
*
|
|
* `ConfigArray` instance contains all settings, parsers, and plugins.
|
|
* You need to call `ConfigArray#extractConfig(filePath)` method in order to
|
|
* extract, merge and get only the config data which is related to an arbitrary
|
|
* file.
|
|
* @extends {Array<ConfigArrayElement>}
|
|
*/
|
|
class ConfigArray extends Array {
|
|
|
|
/**
|
|
* Get the plugin environments.
|
|
* The returned map cannot be mutated.
|
|
* @type {ReadonlyMap<string, Environment>} The plugin environments.
|
|
*/
|
|
get pluginEnvironments() {
|
|
return ensurePluginMemberMaps(this).envMap;
|
|
}
|
|
|
|
/**
|
|
* Get the plugin processors.
|
|
* The returned map cannot be mutated.
|
|
* @type {ReadonlyMap<string, Processor>} The plugin processors.
|
|
*/
|
|
get pluginProcessors() {
|
|
return ensurePluginMemberMaps(this).processorMap;
|
|
}
|
|
|
|
/**
|
|
* Get the plugin rules.
|
|
* The returned map cannot be mutated.
|
|
* @returns {ReadonlyMap<string, Rule>} The plugin rules.
|
|
*/
|
|
get pluginRules() {
|
|
return ensurePluginMemberMaps(this).ruleMap;
|
|
}
|
|
|
|
/**
|
|
* Check if this config has `root` flag.
|
|
* @returns {boolean} `true` if this config array is root.
|
|
*/
|
|
isRoot() {
|
|
for (let i = this.length - 1; i >= 0; --i) {
|
|
const root = this[i].root;
|
|
|
|
if (typeof root === "boolean") {
|
|
return root;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Extract the config data which is related to a given file.
|
|
* @param {string} filePath The absolute path to the target file.
|
|
* @returns {ExtractedConfig} The extracted config data.
|
|
*/
|
|
extractConfig(filePath) {
|
|
const { cache } = internalSlotsMap$2.get(this);
|
|
const indices = getMatchedIndices(this, filePath);
|
|
const cacheKey = indices.join(",");
|
|
|
|
if (!cache.has(cacheKey)) {
|
|
cache.set(cacheKey, createConfig(this, indices));
|
|
}
|
|
|
|
return cache.get(cacheKey);
|
|
}
|
|
|
|
/**
|
|
* Check if a given path is an additional lint target.
|
|
* @param {string} filePath The absolute path to the target file.
|
|
* @returns {boolean} `true` if the file is an additional lint target.
|
|
*/
|
|
isAdditionalTargetPath(filePath) {
|
|
for (const { criteria, type } of this) {
|
|
if (
|
|
type === "config" &&
|
|
criteria &&
|
|
!criteria.endsWithWildcard &&
|
|
criteria.test(filePath)
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the used extracted configs.
|
|
* CLIEngine will use this method to collect used deprecated rules.
|
|
* @param {ConfigArray} instance The config array object to get.
|
|
* @returns {ExtractedConfig[]} The used extracted configs.
|
|
* @private
|
|
*/
|
|
function getUsedExtractedConfigs(instance) {
|
|
const { cache } = internalSlotsMap$2.get(instance);
|
|
|
|
return Array.from(cache.values());
|
|
}
|
|
|
|
/**
|
|
* @fileoverview `ConfigDependency` class.
|
|
*
|
|
* `ConfigDependency` class expresses a loaded parser or plugin.
|
|
*
|
|
* If the parser or plugin was loaded successfully, it has `definition` property
|
|
* and `filePath` property. Otherwise, it has `error` property.
|
|
*
|
|
* When `JSON.stringify()` converted a `ConfigDependency` object to a JSON, it
|
|
* omits `definition` property.
|
|
*
|
|
* `ConfigArrayFactory` creates `ConfigDependency` objects when it loads parsers
|
|
* or plugins.
|
|
*
|
|
* @author Toru Nagashima <https://github.com/mysticatea>
|
|
*/
|
|
|
|
/**
|
|
* The class is to store parsers or plugins.
|
|
* This class hides the loaded object from `JSON.stringify()` and `console.log`.
|
|
* @template T
|
|
*/
|
|
class ConfigDependency {
|
|
|
|
/**
|
|
* Initialize this instance.
|
|
* @param {Object} data The dependency data.
|
|
* @param {T} [data.definition] The dependency if the loading succeeded.
|
|
* @param {Error} [data.error] The error object if the loading failed.
|
|
* @param {string} [data.filePath] The actual path to the dependency if the loading succeeded.
|
|
* @param {string} data.id The ID of this dependency.
|
|
* @param {string} data.importerName The name of the config file which loads this dependency.
|
|
* @param {string} data.importerPath The path to the config file which loads this dependency.
|
|
*/
|
|
constructor({
|
|
definition = null,
|
|
error = null,
|
|
filePath = null,
|
|
id,
|
|
importerName,
|
|
importerPath
|
|
}) {
|
|
|
|
/**
|
|
* The loaded dependency if the loading succeeded.
|
|
* @type {T|null}
|
|
*/
|
|
this.definition = definition;
|
|
|
|
/**
|
|
* The error object if the loading failed.
|
|
* @type {Error|null}
|
|
*/
|
|
this.error = error;
|
|
|
|
/**
|
|
* The loaded dependency if the loading succeeded.
|
|
* @type {string|null}
|
|
*/
|
|
this.filePath = filePath;
|
|
|
|
/**
|
|
* The ID of this dependency.
|
|
* @type {string}
|
|
*/
|
|
this.id = id;
|
|
|
|
/**
|
|
* The name of the config file which loads this dependency.
|
|
* @type {string}
|
|
*/
|
|
this.importerName = importerName;
|
|
|
|
/**
|
|
* The path to the config file which loads this dependency.
|
|
* @type {string}
|
|
*/
|
|
this.importerPath = importerPath;
|
|
}
|
|
|
|
// eslint-disable-next-line jsdoc/require-description
|
|
/**
|
|
* @returns {Object} a JSON compatible object.
|
|
*/
|
|
toJSON() {
|
|
const obj = this[util__default["default"].inspect.custom]();
|
|
|
|
// Display `error.message` (`Error#message` is unenumerable).
|
|
if (obj.error instanceof Error) {
|
|
obj.error = { ...obj.error, message: obj.error.message };
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
// eslint-disable-next-line jsdoc/require-description
|
|
/**
|
|
* @returns {Object} an object to display by `console.log()`.
|
|
*/
|
|
[util__default["default"].inspect.custom]() {
|
|
const {
|
|
definition: _ignore, // eslint-disable-line no-unused-vars
|
|
...obj
|
|
} = this;
|
|
|
|
return obj;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @fileoverview `OverrideTester` class.
|
|
*
|
|
* `OverrideTester` class handles `files` property and `excludedFiles` property
|
|
* of `overrides` config.
|
|
*
|
|
* It provides one method.
|
|
*
|
|
* - `test(filePath)`
|
|
* Test if a file path matches the pair of `files` property and
|
|
* `excludedFiles` property. The `filePath` argument must be an absolute
|
|
* path.
|
|
*
|
|
* `ConfigArrayFactory` creates `OverrideTester` objects when it processes
|
|
* `overrides` properties.
|
|
*
|
|
* @author Toru Nagashima <https://github.com/mysticatea>
|
|
*/
|
|
|
|
const { Minimatch } = minimatch__default["default"];
|
|
|
|
const minimatchOpts = { dot: true, matchBase: true };
|
|
|
|
/**
|
|
* @typedef {Object} Pattern
|
|
* @property {InstanceType<Minimatch>[] | null} includes The positive matchers.
|
|
* @property {InstanceType<Minimatch>[] | null} excludes The negative matchers.
|
|
*/
|
|
|
|
/**
|
|
* Normalize a given pattern to an array.
|
|
* @param {string|string[]|undefined} patterns A glob pattern or an array of glob patterns.
|
|
* @returns {string[]|null} Normalized patterns.
|
|
* @private
|
|
*/
|
|
function normalizePatterns(patterns) {
|
|
if (Array.isArray(patterns)) {
|
|
return patterns.filter(Boolean);
|
|
}
|
|
if (typeof patterns === "string" && patterns) {
|
|
return [patterns];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Create the matchers of given patterns.
|
|
* @param {string[]} patterns The patterns.
|
|
* @returns {InstanceType<Minimatch>[] | null} The matchers.
|
|
*/
|
|
function toMatcher(patterns) {
|
|
if (patterns.length === 0) {
|
|
return null;
|
|
}
|
|
return patterns.map(pattern => {
|
|
if (/^\.[/\\]/u.test(pattern)) {
|
|
return new Minimatch(
|
|
pattern.slice(2),
|
|
|
|
// `./*.js` should not match with `subdir/foo.js`
|
|
{ ...minimatchOpts, matchBase: false }
|
|
);
|
|
}
|
|
return new Minimatch(pattern, minimatchOpts);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Convert a given matcher to string.
|
|
* @param {Pattern} matchers The matchers.
|
|
* @returns {string} The string expression of the matcher.
|
|
*/
|
|
function patternToJson({ includes, excludes }) {
|
|
return {
|
|
includes: includes && includes.map(m => m.pattern),
|
|
excludes: excludes && excludes.map(m => m.pattern)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* The class to test given paths are matched by the patterns.
|
|
*/
|
|
class OverrideTester {
|
|
|
|
/**
|
|
* Create a tester with given criteria.
|
|
* If there are no criteria, returns `null`.
|
|
* @param {string|string[]} files The glob patterns for included files.
|
|
* @param {string|string[]} excludedFiles The glob patterns for excluded files.
|
|
* @param {string} basePath The path to the base directory to test paths.
|
|
* @returns {OverrideTester|null} The created instance or `null`.
|
|
*/
|
|
static create(files, excludedFiles, basePath) {
|
|
const includePatterns = normalizePatterns(files);
|
|
const excludePatterns = normalizePatterns(excludedFiles);
|
|
let endsWithWildcard = false;
|
|
|
|
if (includePatterns.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
// Rejects absolute paths or relative paths to parents.
|
|
for (const pattern of includePatterns) {
|
|
if (path__default["default"].isAbsolute(pattern) || pattern.includes("..")) {
|
|
throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
|
|
}
|
|
if (pattern.endsWith("*")) {
|
|
endsWithWildcard = true;
|
|
}
|
|
}
|
|
for (const pattern of excludePatterns) {
|
|
if (path__default["default"].isAbsolute(pattern) || pattern.includes("..")) {
|
|
throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
|
|
}
|
|
}
|
|
|
|
const includes = toMatcher(includePatterns);
|
|
const excludes = toMatcher(excludePatterns);
|
|
|
|
return new OverrideTester(
|
|
[{ includes, excludes }],
|
|
basePath,
|
|
endsWithWildcard
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Combine two testers by logical and.
|
|
* If either of the testers was `null`, returns the other tester.
|
|
* The `basePath` property of the two must be the same value.
|
|
* @param {OverrideTester|null} a A tester.
|
|
* @param {OverrideTester|null} b Another tester.
|
|
* @returns {OverrideTester|null} Combined tester.
|
|
*/
|
|
static and(a, b) {
|
|
if (!b) {
|
|
return a && new OverrideTester(
|
|
a.patterns,
|
|
a.basePath,
|
|
a.endsWithWildcard
|
|
);
|
|
}
|
|
if (!a) {
|
|
return new OverrideTester(
|
|
b.patterns,
|
|
b.basePath,
|
|
b.endsWithWildcard
|
|
);
|
|
}
|
|
|
|
assert__default["default"].strictEqual(a.basePath, b.basePath);
|
|
return new OverrideTester(
|
|
a.patterns.concat(b.patterns),
|
|
a.basePath,
|
|
a.endsWithWildcard || b.endsWithWildcard
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Initialize this instance.
|
|
* @param {Pattern[]} patterns The matchers.
|
|
* @param {string} basePath The base path.
|
|
* @param {boolean} endsWithWildcard If `true` then a pattern ends with `*`.
|
|
*/
|
|
constructor(patterns, basePath, endsWithWildcard = false) {
|
|
|
|
/** @type {Pattern[]} */
|
|
this.patterns = patterns;
|
|
|
|
/** @type {string} */
|
|
this.basePath = basePath;
|
|
|
|
/** @type {boolean} */
|
|
this.endsWithWildcard = endsWithWildcard;
|
|
}
|
|
|
|
/**
|
|
* Test if a given path is matched or not.
|
|
* @param {string} filePath The absolute path to the target file.
|
|
* @returns {boolean} `true` if the path was matched.
|
|
*/
|
|
test(filePath) {
|
|
if (typeof filePath !== "string" || !path__default["default"].isAbsolute(filePath)) {
|
|
throw new Error(`'filePath' should be an absolute path, but got ${filePath}.`);
|
|
}
|
|
const relativePath = path__default["default"].relative(this.basePath, filePath);
|
|
|
|
return this.patterns.every(({ includes, excludes }) => (
|
|
(!includes || includes.some(m => m.match(relativePath))) &&
|
|
(!excludes || !excludes.some(m => m.match(relativePath)))
|
|
));
|
|
}
|
|
|
|
// eslint-disable-next-line jsdoc/require-description
|
|
/**
|
|
* @returns {Object} a JSON compatible object.
|
|
*/
|
|
toJSON() {
|
|
if (this.patterns.length === 1) {
|
|
return {
|
|
...patternToJson(this.patterns[0]),
|
|
basePath: this.basePath
|
|
};
|
|
}
|
|
return {
|
|
AND: this.patterns.map(patternToJson),
|
|
basePath: this.basePath
|
|
};
|
|
}
|
|
|
|
// eslint-disable-next-line jsdoc/require-description
|
|
/**
|
|
* @returns {Object} an object to display by `console.log()`.
|
|
*/
|
|
[util__default["default"].inspect.custom]() {
|
|
return this.toJSON();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @fileoverview `ConfigArray` class.
|
|
* @author Toru Nagashima <https://github.com/mysticatea>
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview Config file operations. This file must be usable in the browser,
|
|
* so no Node-specific code can be here.
|
|
* @author Nicholas C. Zakas
|
|
*/
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Private
|
|
//------------------------------------------------------------------------------
|
|
|
|
const RULE_SEVERITY_STRINGS = ["off", "warn", "error"],
|
|
RULE_SEVERITY = RULE_SEVERITY_STRINGS.reduce((map, value, index) => {
|
|
map[value] = index;
|
|
return map;
|
|
}, {}),
|
|
VALID_SEVERITIES = [0, 1, 2, "off", "warn", "error"];
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Public Interface
|
|
//------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Normalizes the severity value of a rule's configuration to a number
|
|
* @param {(number|string|[number, ...*]|[string, ...*])} ruleConfig A rule's configuration value, generally
|
|
* received from the user. A valid config value is either 0, 1, 2, the string "off" (treated the same as 0),
|
|
* the string "warn" (treated the same as 1), the string "error" (treated the same as 2), or an array
|
|
* whose first element is one of the above values. Strings are matched case-insensitively.
|
|
* @returns {(0|1|2)} The numeric severity value if the config value was valid, otherwise 0.
|
|
*/
|
|
function getRuleSeverity(ruleConfig) {
|
|
const severityValue = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig;
|
|
|
|
if (severityValue === 0 || severityValue === 1 || severityValue === 2) {
|
|
return severityValue;
|
|
}
|
|
|
|
if (typeof severityValue === "string") {
|
|
return RULE_SEVERITY[severityValue.toLowerCase()] || 0;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Converts old-style severity settings (0, 1, 2) into new-style
|
|
* severity settings (off, warn, error) for all rules. Assumption is that severity
|
|
* values have already been validated as correct.
|
|
* @param {Object} config The config object to normalize.
|
|
* @returns {void}
|
|
*/
|
|
function normalizeToStrings(config) {
|
|
|
|
if (config.rules) {
|
|
Object.keys(config.rules).forEach(ruleId => {
|
|
const ruleConfig = config.rules[ruleId];
|
|
|
|
if (typeof ruleConfig === "number") {
|
|
config.rules[ruleId] = RULE_SEVERITY_STRINGS[ruleConfig] || RULE_SEVERITY_STRINGS[0];
|
|
} else if (Array.isArray(ruleConfig) && typeof ruleConfig[0] === "number") {
|
|
ruleConfig[0] = RULE_SEVERITY_STRINGS[ruleConfig[0]] || RULE_SEVERITY_STRINGS[0];
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines if the severity for the given rule configuration represents an error.
|
|
* @param {int|string|Array} ruleConfig The configuration for an individual rule.
|
|
* @returns {boolean} True if the rule represents an error, false if not.
|
|
*/
|
|
function isErrorSeverity(ruleConfig) {
|
|
return getRuleSeverity(ruleConfig) === 2;
|
|
}
|
|
|
|
/**
|
|
* Checks whether a given config has valid severity or not.
|
|
* @param {number|string|Array} ruleConfig The configuration for an individual rule.
|
|
* @returns {boolean} `true` if the configuration has valid severity.
|
|
*/
|
|
function isValidSeverity(ruleConfig) {
|
|
let severity = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig;
|
|
|
|
if (typeof severity === "string") {
|
|
severity = severity.toLowerCase();
|
|
}
|
|
return VALID_SEVERITIES.indexOf(severity) !== -1;
|
|
}
|
|
|
|
/**
|
|
* Checks whether every rule of a given config has valid severity or not.
|
|
* @param {Object} config The configuration for rules.
|
|
* @returns {boolean} `true` if the configuration has valid severity.
|
|
*/
|
|
function isEverySeverityValid(config) {
|
|
return Object.keys(config).every(ruleId => isValidSeverity(config[ruleId]));
|
|
}
|
|
|
|
/**
|
|
* Normalizes a value for a global in a config
|
|
* @param {(boolean|string|null)} configuredValue The value given for a global in configuration or in
|
|
* a global directive comment
|
|
* @returns {("readable"|"writeable"|"off")} The value normalized as a string
|
|
* @throws Error if global value is invalid
|
|
*/
|
|
function normalizeConfigGlobal(configuredValue) {
|
|
switch (configuredValue) {
|
|
case "off":
|
|
return "off";
|
|
|
|
case true:
|
|
case "true":
|
|
case "writeable":
|
|
case "writable":
|
|
return "writable";
|
|
|
|
case null:
|
|
case false:
|
|
case "false":
|
|
case "readable":
|
|
case "readonly":
|
|
return "readonly";
|
|
|
|
default:
|
|
throw new Error(`'${configuredValue}' is not a valid configuration for a global (use 'readonly', 'writable', or 'off')`);
|
|
}
|
|
}
|
|
|
|
var ConfigOps = {
|
|
__proto__: null,
|
|
getRuleSeverity: getRuleSeverity,
|
|
normalizeToStrings: normalizeToStrings,
|
|
isErrorSeverity: isErrorSeverity,
|
|
isValidSeverity: isValidSeverity,
|
|
isEverySeverityValid: isEverySeverityValid,
|
|
normalizeConfigGlobal: normalizeConfigGlobal
|
|
};
|
|
|
|
/**
|
|
* @fileoverview Provide the function that emits deprecation warnings.
|
|
* @author Toru Nagashima <http://github.com/mysticatea>
|
|
*/
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Private
|
|
//------------------------------------------------------------------------------
|
|
|
|
// Defitions for deprecation warnings.
|
|
const deprecationWarningMessages = {
|
|
ESLINT_LEGACY_ECMAFEATURES:
|
|
"The 'ecmaFeatures' config file property is deprecated and has no effect.",
|
|
ESLINT_PERSONAL_CONFIG_LOAD:
|
|
"'~/.eslintrc.*' config files have been deprecated. " +
|
|
"Please use a config file per project or the '--config' option.",
|
|
ESLINT_PERSONAL_CONFIG_SUPPRESS:
|
|
"'~/.eslintrc.*' config files have been deprecated. " +
|
|
"Please remove it or add 'root:true' to the config files in your " +
|
|
"projects in order to avoid loading '~/.eslintrc.*' accidentally."
|
|
};
|
|
|
|
const sourceFileErrorCache = new Set();
|
|
|
|
/**
|
|
* Emits a deprecation warning containing a given filepath. A new deprecation warning is emitted
|
|
* for each unique file path, but repeated invocations with the same file path have no effect.
|
|
* No warnings are emitted if the `--no-deprecation` or `--no-warnings` Node runtime flags are active.
|
|
* @param {string} source The name of the configuration source to report the warning for.
|
|
* @param {string} errorCode The warning message to show.
|
|
* @returns {void}
|
|
*/
|
|
function emitDeprecationWarning(source, errorCode) {
|
|
const cacheKey = JSON.stringify({ source, errorCode });
|
|
|
|
if (sourceFileErrorCache.has(cacheKey)) {
|
|
return;
|
|
}
|
|
sourceFileErrorCache.add(cacheKey);
|
|
|
|
const rel = path__default["default"].relative(process.cwd(), source);
|
|
const message = deprecationWarningMessages[errorCode];
|
|
|
|
process.emitWarning(
|
|
`${message} (found in "${rel}")`,
|
|
"DeprecationWarning",
|
|
errorCode
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @fileoverview The instance of Ajv validator.
|
|
* @author Evgeny Poberezkin
|
|
*/
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Helpers
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/*
|
|
* Copied from ajv/lib/refs/json-schema-draft-04.json
|
|
* The MIT License (MIT)
|
|
* Copyright (c) 2015-2017 Evgeny Poberezkin
|
|
*/
|
|
const metaSchema = {
|
|
id: "http://json-schema.org/draft-04/schema#",
|
|
$schema: "http://json-schema.org/draft-04/schema#",
|
|
description: "Core schema meta-schema",
|
|
definitions: {
|
|
schemaArray: {
|
|
type: "array",
|
|
minItems: 1,
|
|
items: { $ref: "#" }
|
|
},
|
|
positiveInteger: {
|
|
type: "integer",
|
|
minimum: 0
|
|
},
|
|
positiveIntegerDefault0: {
|
|
allOf: [{ $ref: "#/definitions/positiveInteger" }, { default: 0 }]
|
|
},
|
|
simpleTypes: {
|
|
enum: ["array", "boolean", "integer", "null", "number", "object", "string"]
|
|
},
|
|
stringArray: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
minItems: 1,
|
|
uniqueItems: true
|
|
}
|
|
},
|
|
type: "object",
|
|
properties: {
|
|
id: {
|
|
type: "string"
|
|
},
|
|
$schema: {
|
|
type: "string"
|
|
},
|
|
title: {
|
|
type: "string"
|
|
},
|
|
description: {
|
|
type: "string"
|
|
},
|
|
default: { },
|
|
multipleOf: {
|
|
type: "number",
|
|
minimum: 0,
|
|
exclusiveMinimum: true
|
|
},
|
|
maximum: {
|
|
type: "number"
|
|
},
|
|
exclusiveMaximum: {
|
|
type: "boolean",
|
|
default: false
|
|
},
|
|
minimum: {
|
|
type: "number"
|
|
},
|
|
exclusiveMinimum: {
|
|
type: "boolean",
|
|
default: false
|
|
},
|
|
maxLength: { $ref: "#/definitions/positiveInteger" },
|
|
minLength: { $ref: "#/definitions/positiveIntegerDefault0" },
|
|
pattern: {
|
|
type: "string",
|
|
format: "regex"
|
|
},
|
|
additionalItems: {
|
|
anyOf: [
|
|
{ type: "boolean" },
|
|
{ $ref: "#" }
|
|
],
|
|
default: { }
|
|
},
|
|
items: {
|
|
anyOf: [
|
|
{ $ref: "#" },
|
|
{ $ref: "#/definitions/schemaArray" }
|
|
],
|
|
default: { }
|
|
},
|
|
maxItems: { $ref: "#/definitions/positiveInteger" },
|
|
minItems: { $ref: "#/definitions/positiveIntegerDefault0" },
|
|
uniqueItems: {
|
|
type: "boolean",
|
|
default: false
|
|
},
|
|
maxProperties: { $ref: "#/definitions/positiveInteger" },
|
|
minProperties: { $ref: "#/definitions/positiveIntegerDefault0" },
|
|
required: { $ref: "#/definitions/stringArray" },
|
|
additionalProperties: {
|
|
anyOf: [
|
|
{ type: "boolean" },
|
|
{ $ref: "#" }
|
|
],
|
|
default: { }
|
|
},
|
|
definitions: {
|
|
type: "object",
|
|
additionalProperties: { $ref: "#" },
|
|
default: { }
|
|
},
|
|
properties: {
|
|
type: "object",
|
|
additionalProperties: { $ref: "#" },
|
|
default: { }
|
|
},
|
|
patternProperties: {
|
|
type: "object",
|
|
additionalProperties: { $ref: "#" },
|
|
default: { }
|
|
},
|
|
dependencies: {
|
|
type: "object",
|
|
additionalProperties: {
|
|
anyOf: [
|
|
{ $ref: "#" },
|
|
{ $ref: "#/definitions/stringArray" }
|
|
]
|
|
}
|
|
},
|
|
enum: {
|
|
type: "array",
|
|
minItems: 1,
|
|
uniqueItems: true
|
|
},
|
|
type: {
|
|
anyOf: [
|
|
{ $ref: "#/definitions/simpleTypes" },
|
|
{
|
|
type: "array",
|
|
items: { $ref: "#/definitions/simpleTypes" },
|
|
minItems: 1,
|
|
uniqueItems: true
|
|
}
|
|
]
|
|
},
|
|
format: { type: "string" },
|
|
allOf: { $ref: "#/definitions/schemaArray" },
|
|
anyOf: { $ref: "#/definitions/schemaArray" },
|
|
oneOf: { $ref: "#/definitions/schemaArray" },
|
|
not: { $ref: "#" }
|
|
},
|
|
dependencies: {
|
|
exclusiveMaximum: ["maximum"],
|
|
exclusiveMinimum: ["minimum"]
|
|
},
|
|
default: { }
|
|
};
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Public Interface
|
|
//------------------------------------------------------------------------------
|
|
|
|
var ajvOrig = (additionalOptions = {}) => {
|
|
const ajv = new Ajv__default["default"]({
|
|
meta: false,
|
|
useDefaults: true,
|
|
validateSchema: false,
|
|
missingRefs: "ignore",
|
|
verbose: true,
|
|
schemaId: "auto",
|
|
...additionalOptions
|
|
});
|
|
|
|
ajv.addMetaSchema(metaSchema);
|
|
// eslint-disable-next-line no-underscore-dangle
|
|
ajv._opts.defaultMeta = metaSchema.id;
|
|
|
|
return ajv;
|
|
};
|
|
|
|
/**
|
|
* @fileoverview Defines a schema for configs.
|
|
* @author Sylvan Mably
|
|
*/
|
|
|
|
const baseConfigProperties = {
|
|
$schema: { type: "string" },
|
|
env: { type: "object" },
|
|
extends: { $ref: "#/definitions/stringOrStrings" },
|
|
globals: { type: "object" },
|
|
overrides: {
|
|
type: "array",
|
|
items: { $ref: "#/definitions/overrideConfig" },
|
|
additionalItems: false
|
|
},
|
|
parser: { type: ["string", "null"] },
|
|
parserOptions: { type: "object" },
|
|
plugins: { type: "array" },
|
|
processor: { type: "string" },
|
|
rules: { type: "object" },
|
|
settings: { type: "object" },
|
|
noInlineConfig: { type: "boolean" },
|
|
reportUnusedDisableDirectives: { type: "boolean" },
|
|
|
|
ecmaFeatures: { type: "object" } // deprecated; logs a warning when used
|
|
};
|
|
|
|
const configSchema = {
|
|
definitions: {
|
|
stringOrStrings: {
|
|
oneOf: [
|
|
{ type: "string" },
|
|
{
|
|
type: "array",
|
|
items: { type: "string" },
|
|
additionalItems: false
|
|
}
|
|
]
|
|
},
|
|
stringOrStringsRequired: {
|
|
oneOf: [
|
|
{ type: "string" },
|
|
{
|
|
type: "array",
|
|
items: { type: "string" },
|
|
additionalItems: false,
|
|
minItems: 1
|
|
}
|
|
]
|
|
},
|
|
|
|
// Config at top-level.
|
|
objectConfig: {
|
|
type: "object",
|
|
properties: {
|
|
root: { type: "boolean" },
|
|
ignorePatterns: { $ref: "#/definitions/stringOrStrings" },
|
|
...baseConfigProperties
|
|
},
|
|
additionalProperties: false
|
|
},
|
|
|
|
// Config in `overrides`.
|
|
overrideConfig: {
|
|
type: "object",
|
|
properties: {
|
|
excludedFiles: { $ref: "#/definitions/stringOrStrings" },
|
|
files: { $ref: "#/definitions/stringOrStringsRequired" },
|
|
...baseConfigProperties
|
|
},
|
|
required: ["files"],
|
|
additionalProperties: false
|
|
}
|
|
},
|
|
|
|
$ref: "#/definitions/objectConfig"
|
|
};
|
|
|
|
/**
|
|
* @fileoverview Defines environment settings and globals.
|
|
* @author Elan Shanker
|
|
*/
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Helpers
|
|
//------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Get the object that has difference.
|
|
* @param {Record<string,boolean>} current The newer object.
|
|
* @param {Record<string,boolean>} prev The older object.
|
|
* @returns {Record<string,boolean>} The difference object.
|
|
*/
|
|
function getDiff(current, prev) {
|
|
const retv = {};
|
|
|
|
for (const [key, value] of Object.entries(current)) {
|
|
if (!Object.hasOwnProperty.call(prev, key)) {
|
|
retv[key] = value;
|
|
}
|
|
}
|
|
|
|
return retv;
|
|
}
|
|
|
|
const newGlobals2015 = getDiff(globals__default["default"].es2015, globals__default["default"].es5); // 19 variables such as Promise, Map, ...
|
|
const newGlobals2017 = {
|
|
Atomics: false,
|
|
SharedArrayBuffer: false
|
|
};
|
|
const newGlobals2020 = {
|
|
BigInt: false,
|
|
BigInt64Array: false,
|
|
BigUint64Array: false,
|
|
globalThis: false
|
|
};
|
|
|
|
const newGlobals2021 = {
|
|
AggregateError: false,
|
|
FinalizationRegistry: false,
|
|
WeakRef: false
|
|
};
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Public Interface
|
|
//------------------------------------------------------------------------------
|
|
|
|
/** @type {Map<string, import("../lib/shared/types").Environment>} */
|
|
var environments = new Map(Object.entries({
|
|
|
|
// Language
|
|
builtin: {
|
|
globals: globals__default["default"].es5
|
|
},
|
|
es6: {
|
|
globals: newGlobals2015,
|
|
parserOptions: {
|
|
ecmaVersion: 6
|
|
}
|
|
},
|
|
es2015: {
|
|
globals: newGlobals2015,
|
|
parserOptions: {
|
|
ecmaVersion: 6
|
|
}
|
|
},
|
|
es2016: {
|
|
globals: newGlobals2015,
|
|
parserOptions: {
|
|
ecmaVersion: 7
|
|
}
|
|
},
|
|
es2017: {
|
|
globals: { ...newGlobals2015, ...newGlobals2017 },
|
|
parserOptions: {
|
|
ecmaVersion: 8
|
|
}
|
|
},
|
|
es2018: {
|
|
globals: { ...newGlobals2015, ...newGlobals2017 },
|
|
parserOptions: {
|
|
ecmaVersion: 9
|
|
}
|
|
},
|
|
es2019: {
|
|
globals: { ...newGlobals2015, ...newGlobals2017 },
|
|
parserOptions: {
|
|
ecmaVersion: 10
|
|
}
|
|
},
|
|
es2020: {
|
|
globals: { ...newGlobals2015, ...newGlobals2017, ...newGlobals2020 },
|
|
parserOptions: {
|
|
ecmaVersion: 11
|
|
}
|
|
},
|
|
es2021: {
|
|
globals: { ...newGlobals2015, ...newGlobals2017, ...newGlobals2020, ...newGlobals2021 },
|
|
parserOptions: {
|
|
ecmaVersion: 12
|
|
}
|
|
},
|
|
es2022: {
|
|
globals: { ...newGlobals2015, ...newGlobals2017, ...newGlobals2020, ...newGlobals2021 },
|
|
parserOptions: {
|
|
ecmaVersion: 13
|
|
}
|
|
},
|
|
|
|
// Platforms
|
|
browser: {
|
|
globals: globals__default["default"].browser
|
|
},
|
|
node: {
|
|
globals: globals__default["default"].node,
|
|
parserOptions: {
|
|
ecmaFeatures: {
|
|
globalReturn: true
|
|
}
|
|
}
|
|
},
|
|
"shared-node-browser": {
|
|
globals: globals__default["default"]["shared-node-browser"]
|
|
},
|
|
worker: {
|
|
globals: globals__default["default"].worker
|
|
},
|
|
serviceworker: {
|
|
globals: globals__default["default"].serviceworker
|
|
},
|
|
|
|
// Frameworks
|
|
commonjs: {
|
|
globals: globals__default["default"].commonjs,
|
|
parserOptions: {
|
|
ecmaFeatures: {
|
|
globalReturn: true
|
|
}
|
|
}
|
|
},
|
|
amd: {
|
|
globals: globals__default["default"].amd
|
|
},
|
|
mocha: {
|
|
globals: globals__default["default"].mocha
|
|
},
|
|
jasmine: {
|
|
globals: globals__default["default"].jasmine
|
|
},
|
|
jest: {
|
|
globals: globals__default["default"].jest
|
|
},
|
|
phantomjs: {
|
|
globals: globals__default["default"].phantomjs
|
|
},
|
|
jquery: {
|
|
globals: globals__default["default"].jquery
|
|
},
|
|
qunit: {
|
|
globals: globals__default["default"].qunit
|
|
},
|
|
prototypejs: {
|
|
globals: globals__default["default"].prototypejs
|
|
},
|
|
shelljs: {
|
|
globals: globals__default["default"].shelljs
|
|
},
|
|
meteor: {
|
|
globals: globals__default["default"].meteor
|
|
},
|
|
mongo: {
|
|
globals: globals__default["default"].mongo
|
|
},
|
|
protractor: {
|
|
globals: globals__default["default"].protractor
|
|
},
|
|
applescript: {
|
|
globals: globals__default["default"].applescript
|
|
},
|
|
nashorn: {
|
|
globals: globals__default["default"].nashorn
|
|
},
|
|
atomtest: {
|
|
globals: globals__default["default"].atomtest
|
|
},
|
|
embertest: {
|
|
globals: globals__default["default"].embertest
|
|
},
|
|
webextensions: {
|
|
globals: globals__default["default"].webextensions
|
|
},
|
|
greasemonkey: {
|
|
globals: globals__default["default"].greasemonkey
|
|
}
|
|
}));
|
|
|
|
/**
|
|
* @fileoverview Validates configs.
|
|
* @author Brandon Mills
|
|
*/
|
|
|
|
const ajv = ajvOrig();
|
|
|
|
const ruleValidators = new WeakMap();
|
|
const noop = Function.prototype;
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Private
|
|
//------------------------------------------------------------------------------
|
|
let validateSchema;
|
|
const severityMap = {
|
|
error: 2,
|
|
warn: 1,
|
|
off: 0
|
|
};
|
|
|
|
const validated = new WeakSet();
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Exports
|
|
//-----------------------------------------------------------------------------
|
|
|
|
class ConfigValidator {
|
|
constructor({ builtInRules = new Map() } = {}) {
|
|
this.builtInRules = builtInRules;
|
|
}
|
|
|
|
/**
|
|
* Gets a complete options schema for a rule.
|
|
* @param {{create: Function, schema: (Array|null)}} rule A new-style rule object
|
|
* @returns {Object} JSON Schema for the rule's options.
|
|
*/
|
|
getRuleOptionsSchema(rule) {
|
|
if (!rule) {
|
|
return null;
|
|
}
|
|
|
|
const schema = rule.schema || rule.meta && rule.meta.schema;
|
|
|
|
// Given a tuple of schemas, insert warning level at the beginning
|
|
if (Array.isArray(schema)) {
|
|
if (schema.length) {
|
|
return {
|
|
type: "array",
|
|
items: schema,
|
|
minItems: 0,
|
|
maxItems: schema.length
|
|
};
|
|
}
|
|
return {
|
|
type: "array",
|
|
minItems: 0,
|
|
maxItems: 0
|
|
};
|
|
|
|
}
|
|
|
|
// Given a full schema, leave it alone
|
|
return schema || null;
|
|
}
|
|
|
|
/**
|
|
* Validates a rule's severity and returns the severity value. Throws an error if the severity is invalid.
|
|
* @param {options} options The given options for the rule.
|
|
* @returns {number|string} The rule's severity value
|
|
*/
|
|
validateRuleSeverity(options) {
|
|
const severity = Array.isArray(options) ? options[0] : options;
|
|
const normSeverity = typeof severity === "string" ? severityMap[severity.toLowerCase()] : severity;
|
|
|
|
if (normSeverity === 0 || normSeverity === 1 || normSeverity === 2) {
|
|
return normSeverity;
|
|
}
|
|
|
|
throw new Error(`\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '${util__default["default"].inspect(severity).replace(/'/gu, "\"").replace(/\n/gu, "")}').\n`);
|
|
|
|
}
|
|
|
|
/**
|
|
* Validates the non-severity options passed to a rule, based on its schema.
|
|
* @param {{create: Function}} rule The rule to validate
|
|
* @param {Array} localOptions The options for the rule, excluding severity
|
|
* @returns {void}
|
|
*/
|
|
validateRuleSchema(rule, localOptions) {
|
|
if (!ruleValidators.has(rule)) {
|
|
const schema = this.getRuleOptionsSchema(rule);
|
|
|
|
if (schema) {
|
|
ruleValidators.set(rule, ajv.compile(schema));
|
|
}
|
|
}
|
|
|
|
const validateRule = ruleValidators.get(rule);
|
|
|
|
if (validateRule) {
|
|
validateRule(localOptions);
|
|
if (validateRule.errors) {
|
|
throw new Error(validateRule.errors.map(
|
|
error => `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`
|
|
).join(""));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates a rule's options against its schema.
|
|
* @param {{create: Function}|null} rule The rule that the config is being validated for
|
|
* @param {string} ruleId The rule's unique name.
|
|
* @param {Array|number} options The given options for the rule.
|
|
* @param {string|null} source The name of the configuration source to report in any errors. If null or undefined,
|
|
* no source is prepended to the message.
|
|
* @returns {void}
|
|
*/
|
|
validateRuleOptions(rule, ruleId, options, source = null) {
|
|
try {
|
|
const severity = this.validateRuleSeverity(options);
|
|
|
|
if (severity !== 0) {
|
|
this.validateRuleSchema(rule, Array.isArray(options) ? options.slice(1) : []);
|
|
}
|
|
} catch (err) {
|
|
const enhancedMessage = `Configuration for rule "${ruleId}" is invalid:\n${err.message}`;
|
|
|
|
if (typeof source === "string") {
|
|
throw new Error(`${source}:\n\t${enhancedMessage}`);
|
|
} else {
|
|
throw new Error(enhancedMessage);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates an environment object
|
|
* @param {Object} environment The environment config object to validate.
|
|
* @param {string} source The name of the configuration source to report in any errors.
|
|
* @param {function(envId:string): Object} [getAdditionalEnv] A map from strings to loaded environments.
|
|
* @returns {void}
|
|
*/
|
|
validateEnvironment(
|
|
environment,
|
|
source,
|
|
getAdditionalEnv = noop
|
|
) {
|
|
|
|
// not having an environment is ok
|
|
if (!environment) {
|
|
return;
|
|
}
|
|
|
|
Object.keys(environment).forEach(id => {
|
|
const env = getAdditionalEnv(id) || environments.get(id) || null;
|
|
|
|
if (!env) {
|
|
const message = `${source}:\n\tEnvironment key "${id}" is unknown\n`;
|
|
|
|
throw new Error(message);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Validates a rules config object
|
|
* @param {Object} rulesConfig The rules config object to validate.
|
|
* @param {string} source The name of the configuration source to report in any errors.
|
|
* @param {function(ruleId:string): Object} getAdditionalRule A map from strings to loaded rules
|
|
* @returns {void}
|
|
*/
|
|
validateRules(
|
|
rulesConfig,
|
|
source,
|
|
getAdditionalRule = noop
|
|
) {
|
|
if (!rulesConfig) {
|
|
return;
|
|
}
|
|
|
|
Object.keys(rulesConfig).forEach(id => {
|
|
const rule = getAdditionalRule(id) || this.builtInRules.get(id) || null;
|
|
|
|
this.validateRuleOptions(rule, id, rulesConfig[id], source);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Validates a `globals` section of a config file
|
|
* @param {Object} globalsConfig The `globals` section
|
|
* @param {string|null} source The name of the configuration source to report in the event of an error.
|
|
* @returns {void}
|
|
*/
|
|
validateGlobals(globalsConfig, source = null) {
|
|
if (!globalsConfig) {
|
|
return;
|
|
}
|
|
|
|
Object.entries(globalsConfig)
|
|
.forEach(([configuredGlobal, configuredValue]) => {
|
|
try {
|
|
normalizeConfigGlobal(configuredValue);
|
|
} catch (err) {
|
|
throw new Error(`ESLint configuration of global '${configuredGlobal}' in ${source} is invalid:\n${err.message}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Validate `processor` configuration.
|
|
* @param {string|undefined} processorName The processor name.
|
|
* @param {string} source The name of config file.
|
|
* @param {function(id:string): Processor} getProcessor The getter of defined processors.
|
|
* @returns {void}
|
|
*/
|
|
validateProcessor(processorName, source, getProcessor) {
|
|
if (processorName && !getProcessor(processorName)) {
|
|
throw new Error(`ESLint configuration of processor in '${source}' is invalid: '${processorName}' was not found.`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Formats an array of schema validation errors.
|
|
* @param {Array} errors An array of error messages to format.
|
|
* @returns {string} Formatted error message
|
|
*/
|
|
formatErrors(errors) {
|
|
return errors.map(error => {
|
|
if (error.keyword === "additionalProperties") {
|
|
const formattedPropertyPath = error.dataPath.length ? `${error.dataPath.slice(1)}.${error.params.additionalProperty}` : error.params.additionalProperty;
|
|
|
|
return `Unexpected top-level property "${formattedPropertyPath}"`;
|
|
}
|
|
if (error.keyword === "type") {
|
|
const formattedField = error.dataPath.slice(1);
|
|
const formattedExpectedType = Array.isArray(error.schema) ? error.schema.join("/") : error.schema;
|
|
const formattedValue = JSON.stringify(error.data);
|
|
|
|
return `Property "${formattedField}" is the wrong type (expected ${formattedExpectedType} but got \`${formattedValue}\`)`;
|
|
}
|
|
|
|
const field = error.dataPath[0] === "." ? error.dataPath.slice(1) : error.dataPath;
|
|
|
|
return `"${field}" ${error.message}. Value: ${JSON.stringify(error.data)}`;
|
|
}).map(message => `\t- ${message}.\n`).join("");
|
|
}
|
|
|
|
/**
|
|
* Validates the top level properties of the config object.
|
|
* @param {Object} config The config object to validate.
|
|
* @param {string} source The name of the configuration source to report in any errors.
|
|
* @returns {void}
|
|
*/
|
|
validateConfigSchema(config, source = null) {
|
|
validateSchema = validateSchema || ajv.compile(configSchema);
|
|
|
|
if (!validateSchema(config)) {
|
|
throw new Error(`ESLint configuration in ${source} is invalid:\n${this.formatErrors(validateSchema.errors)}`);
|
|
}
|
|
|
|
if (Object.hasOwnProperty.call(config, "ecmaFeatures")) {
|
|
emitDeprecationWarning(source, "ESLINT_LEGACY_ECMAFEATURES");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates an entire config object.
|
|
* @param {Object} config The config object to validate.
|
|
* @param {string} source The name of the configuration source to report in any errors.
|
|
* @param {function(ruleId:string): Object} [getAdditionalRule] A map from strings to loaded rules.
|
|
* @param {function(envId:string): Object} [getAdditionalEnv] A map from strings to loaded envs.
|
|
* @returns {void}
|
|
*/
|
|
validate(config, source, getAdditionalRule, getAdditionalEnv) {
|
|
this.validateConfigSchema(config, source);
|
|
this.validateRules(config.rules, source, getAdditionalRule);
|
|
this.validateEnvironment(config.env, source, getAdditionalEnv);
|
|
this.validateGlobals(config.globals, source);
|
|
|
|
for (const override of config.overrides || []) {
|
|
this.validateRules(override.rules, source, getAdditionalRule);
|
|
this.validateEnvironment(override.env, source, getAdditionalEnv);
|
|
this.validateGlobals(config.globals, source);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate config array object.
|
|
* @param {ConfigArray} configArray The config array to validate.
|
|
* @returns {void}
|
|
*/
|
|
validateConfigArray(configArray) {
|
|
const getPluginEnv = Map.prototype.get.bind(configArray.pluginEnvironments);
|
|
const getPluginProcessor = Map.prototype.get.bind(configArray.pluginProcessors);
|
|
const getPluginRule = Map.prototype.get.bind(configArray.pluginRules);
|
|
|
|
// Validate.
|
|
for (const element of configArray) {
|
|
if (validated.has(element)) {
|
|
continue;
|
|
}
|
|
validated.add(element);
|
|
|
|
this.validateEnvironment(element.env, element.name, getPluginEnv);
|
|
this.validateGlobals(element.globals, element.name);
|
|
this.validateProcessor(element.processor, element.name, getPluginProcessor);
|
|
this.validateRules(element.rules, element.name, getPluginRule);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* @fileoverview Common helpers for naming of plugins, formatters and configs
|
|
*/
|
|
|
|
const NAMESPACE_REGEX = /^@.*\//iu;
|
|
|
|
/**
|
|
* Brings package name to correct format based on prefix
|
|
* @param {string} name The name of the package.
|
|
* @param {string} prefix Can be either "eslint-plugin", "eslint-config" or "eslint-formatter"
|
|
* @returns {string} Normalized name of the package
|
|
* @private
|
|
*/
|
|
function normalizePackageName(name, prefix) {
|
|
let normalizedName = name;
|
|
|
|
/**
|
|
* On Windows, name can come in with Windows slashes instead of Unix slashes.
|
|
* Normalize to Unix first to avoid errors later on.
|
|
* https://github.com/eslint/eslint/issues/5644
|
|
*/
|
|
if (normalizedName.includes("\\")) {
|
|
normalizedName = normalizedName.replace(/\\/gu, "/");
|
|
}
|
|
|
|
if (normalizedName.charAt(0) === "@") {
|
|
|
|
/**
|
|
* it's a scoped package
|
|
* package name is the prefix, or just a username
|
|
*/
|
|
const scopedPackageShortcutRegex = new RegExp(`^(@[^/]+)(?:/(?:${prefix})?)?$`, "u"),
|
|
scopedPackageNameRegex = new RegExp(`^${prefix}(-|$)`, "u");
|
|
|
|
if (scopedPackageShortcutRegex.test(normalizedName)) {
|
|
normalizedName = normalizedName.replace(scopedPackageShortcutRegex, `$1/${prefix}`);
|
|
} else if (!scopedPackageNameRegex.test(normalizedName.split("/")[1])) {
|
|
|
|
/**
|
|
* for scoped packages, insert the prefix after the first / unless
|
|
* the path is already @scope/eslint or @scope/eslint-xxx-yyy
|
|
*/
|
|
normalizedName = normalizedName.replace(/^@([^/]+)\/(.*)$/u, `@$1/${prefix}-$2`);
|
|
}
|
|
} else if (!normalizedName.startsWith(`${prefix}-`)) {
|
|
normalizedName = `${prefix}-${normalizedName}`;
|
|
}
|
|
|
|
return normalizedName;
|
|
}
|
|
|
|
/**
|
|
* Removes the prefix from a fullname.
|
|
* @param {string} fullname The term which may have the prefix.
|
|
* @param {string} prefix The prefix to remove.
|
|
* @returns {string} The term without prefix.
|
|
*/
|
|
function getShorthandName(fullname, prefix) {
|
|
if (fullname[0] === "@") {
|
|
let matchResult = new RegExp(`^(@[^/]+)/${prefix}$`, "u").exec(fullname);
|
|
|
|
if (matchResult) {
|
|
return matchResult[1];
|
|
}
|
|
|
|
matchResult = new RegExp(`^(@[^/]+)/${prefix}-(.+)$`, "u").exec(fullname);
|
|
if (matchResult) {
|
|
return `${matchResult[1]}/${matchResult[2]}`;
|
|
}
|
|
} else if (fullname.startsWith(`${prefix}-`)) {
|
|
return fullname.slice(prefix.length + 1);
|
|
}
|
|
|
|
return fullname;
|
|
}
|
|
|
|
/**
|
|
* Gets the scope (namespace) of a term.
|
|
* @param {string} term The term which may have the namespace.
|
|
* @returns {string} The namespace of the term if it has one.
|
|
*/
|
|
function getNamespaceFromTerm(term) {
|
|
const match = term.match(NAMESPACE_REGEX);
|
|
|
|
return match ? match[0] : "";
|
|
}
|
|
|
|
var naming = {
|
|
__proto__: null,
|
|
normalizePackageName: normalizePackageName,
|
|
getShorthandName: getShorthandName,
|
|
getNamespaceFromTerm: getNamespaceFromTerm
|
|
};
|
|
|
|
/**
|
|
* Utility for resolving a module relative to another module
|
|
* @author Teddy Katz
|
|
*/
|
|
|
|
/*
|
|
* `Module.createRequire` is added in v12.2.0. It supports URL as well.
|
|
* We only support the case where the argument is a filepath, not a URL.
|
|
*/
|
|
const createRequire = Module__default["default"].createRequire;
|
|
|
|
/**
|
|
* Resolves a Node module relative to another module
|
|
* @param {string} moduleName The name of a Node module, or a path to a Node module.
|
|
* @param {string} relativeToPath An absolute path indicating the module that `moduleName` should be resolved relative to. This must be
|
|
* a file rather than a directory, but the file need not actually exist.
|
|
* @returns {string} The absolute path that would result from calling `require.resolve(moduleName)` in a file located at `relativeToPath`
|
|
*/
|
|
function resolve(moduleName, relativeToPath) {
|
|
try {
|
|
return createRequire(relativeToPath).resolve(moduleName);
|
|
} catch (error) {
|
|
|
|
// This `if` block is for older Node.js than 12.0.0. We can remove this block in the future.
|
|
if (
|
|
typeof error === "object" &&
|
|
error !== null &&
|
|
error.code === "MODULE_NOT_FOUND" &&
|
|
!error.requireStack &&
|
|
error.message.includes(moduleName)
|
|
) {
|
|
error.message += `\nRequire stack:\n- ${relativeToPath}`;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
var ModuleResolver = {
|
|
__proto__: null,
|
|
resolve: resolve
|
|
};
|
|
|
|
/**
|
|
* @fileoverview The factory of `ConfigArray` objects.
|
|
*
|
|
* This class provides methods to create `ConfigArray` instance.
|
|
*
|
|
* - `create(configData, options)`
|
|
* Create a `ConfigArray` instance from a config data. This is to handle CLI
|
|
* options except `--config`.
|
|
* - `loadFile(filePath, options)`
|
|
* Create a `ConfigArray` instance from a config file. This is to handle
|
|
* `--config` option. If the file was not found, throws the following error:
|
|
* - If the filename was `*.js`, a `MODULE_NOT_FOUND` error.
|
|
* - If the filename was `package.json`, an IO error or an
|
|
* `ESLINT_CONFIG_FIELD_NOT_FOUND` error.
|
|
* - Otherwise, an IO error such as `ENOENT`.
|
|
* - `loadInDirectory(directoryPath, options)`
|
|
* Create a `ConfigArray` instance from a config file which is on a given
|
|
* directory. This tries to load `.eslintrc.*` or `package.json`. If not
|
|
* found, returns an empty `ConfigArray`.
|
|
* - `loadESLintIgnore(filePath)`
|
|
* Create a `ConfigArray` instance from a config file that is `.eslintignore`
|
|
* format. This is to handle `--ignore-path` option.
|
|
* - `loadDefaultESLintIgnore()`
|
|
* Create a `ConfigArray` instance from `.eslintignore` or `package.json` in
|
|
* the current working directory.
|
|
*
|
|
* `ConfigArrayFactory` class has the responsibility that loads configuration
|
|
* files, including loading `extends`, `parser`, and `plugins`. The created
|
|
* `ConfigArray` instance has the loaded `extends`, `parser`, and `plugins`.
|
|
*
|
|
* But this class doesn't handle cascading. `CascadingConfigArrayFactory` class
|
|
* handles cascading and hierarchy.
|
|
*
|
|
* @author Toru Nagashima <https://github.com/mysticatea>
|
|
*/
|
|
|
|
const require$1 = Module.createRequire(require('url').pathToFileURL(__filename).toString());
|
|
|
|
const debug$2 = debugOrig__default["default"]("eslintrc:config-array-factory");
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Helpers
|
|
//------------------------------------------------------------------------------
|
|
|
|
const configFilenames = [
|
|
".eslintrc.js",
|
|
".eslintrc.cjs",
|
|
".eslintrc.yaml",
|
|
".eslintrc.yml",
|
|
".eslintrc.json",
|
|
".eslintrc",
|
|
"package.json"
|
|
];
|
|
|
|
// Define types for VSCode IntelliSense.
|
|
/** @typedef {import("./shared/types").ConfigData} ConfigData */
|
|
/** @typedef {import("./shared/types").OverrideConfigData} OverrideConfigData */
|
|
/** @typedef {import("./shared/types").Parser} Parser */
|
|
/** @typedef {import("./shared/types").Plugin} Plugin */
|
|
/** @typedef {import("./shared/types").Rule} Rule */
|
|
/** @typedef {import("./config-array/config-dependency").DependentParser} DependentParser */
|
|
/** @typedef {import("./config-array/config-dependency").DependentPlugin} DependentPlugin */
|
|
/** @typedef {ConfigArray[0]} ConfigArrayElement */
|
|
|
|
/**
|
|
* @typedef {Object} ConfigArrayFactoryOptions
|
|
* @property {Map<string,Plugin>} [additionalPluginPool] The map for additional plugins.
|
|
* @property {string} [cwd] The path to the current working directory.
|
|
* @property {string} [resolvePluginsRelativeTo] A path to the directory that plugins should be resolved from. Defaults to `cwd`.
|
|
* @property {Map<string,Rule>} builtInRules The rules that are built in to ESLint.
|
|
* @property {Object} [resolver=ModuleResolver] The module resolver object.
|
|
* @property {string} eslintAllPath The path to the definitions for eslint:all.
|
|
* @property {Function} getEslintAllConfig Returns the config data for eslint:all.
|
|
* @property {string} eslintRecommendedPath The path to the definitions for eslint:recommended.
|
|
* @property {Function} getEslintRecommendedConfig Returns the config data for eslint:recommended.
|
|
*/
|
|
|
|
/**
|
|
* @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 | undefined} resolvePluginsRelativeTo An absolute path the the directory that plugins should be resolved from.
|
|
* @property {Map<string,Rule>} builtInRules The rules that are built in to ESLint.
|
|
* @property {Object} [resolver=ModuleResolver] The module resolver object.
|
|
* @property {string} eslintAllPath The path to the definitions for eslint:all.
|
|
* @property {Function} getEslintAllConfig Returns the config data for eslint:all.
|
|
* @property {string} eslintRecommendedPath The path to the definitions for eslint:recommended.
|
|
* @property {Function} getEslintRecommendedConfig Returns the config data for eslint:recommended.
|
|
*/
|
|
|
|
/**
|
|
* @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.
|
|
*/
|
|
|
|
/**
|
|
* @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 {"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.
|
|
*/
|
|
|
|
/** @type {WeakMap<ConfigArrayFactory, ConfigArrayFactoryInternalSlots>} */
|
|
const internalSlotsMap$1 = new WeakMap();
|
|
|
|
/**
|
|
* Check if a given string is a file path.
|
|
* @param {string} nameOrPath A module name or file path.
|
|
* @returns {boolean} `true` if the `nameOrPath` is a file path.
|
|
*/
|
|
function isFilePath(nameOrPath) {
|
|
return (
|
|
/^\.{1,2}[/\\]/u.test(nameOrPath) ||
|
|
path__default["default"].isAbsolute(nameOrPath)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Convenience wrapper for synchronously reading file contents.
|
|
* @param {string} filePath The filename to read.
|
|
* @returns {string} The file contents, with the BOM removed.
|
|
* @private
|
|
*/
|
|
function readFile(filePath) {
|
|
return fs__default["default"].readFileSync(filePath, "utf8").replace(/^\ufeff/u, "");
|
|
}
|
|
|
|
/**
|
|
* Loads a YAML configuration from a file.
|
|
* @param {string} filePath The filename to load.
|
|
* @returns {ConfigData} The configuration object from the file.
|
|
* @throws {Error} If the file cannot be read.
|
|
* @private
|
|
*/
|
|
function loadYAMLConfigFile(filePath) {
|
|
debug$2(`Loading YAML config file: ${filePath}`);
|
|
|
|
// lazy load YAML to improve performance when not used
|
|
const yaml = require$1("js-yaml");
|
|
|
|
try {
|
|
|
|
// empty YAML file can be null, so always use
|
|
return yaml.load(readFile(filePath)) || {};
|
|
} catch (e) {
|
|
debug$2(`Error reading YAML file: ${filePath}`);
|
|
e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads a JSON configuration from a file.
|
|
* @param {string} filePath The filename to load.
|
|
* @returns {ConfigData} The configuration object from the file.
|
|
* @throws {Error} If the file cannot be read.
|
|
* @private
|
|
*/
|
|
function loadJSONConfigFile(filePath) {
|
|
debug$2(`Loading JSON config file: ${filePath}`);
|
|
|
|
try {
|
|
return JSON.parse(stripComments__default["default"](readFile(filePath)));
|
|
} catch (e) {
|
|
debug$2(`Error reading JSON file: ${filePath}`);
|
|
e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
|
|
e.messageTemplate = "failed-to-read-json";
|
|
e.messageData = {
|
|
path: filePath,
|
|
message: e.message
|
|
};
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads a legacy (.eslintrc) configuration from a file.
|
|
* @param {string} filePath The filename to load.
|
|
* @returns {ConfigData} The configuration object from the file.
|
|
* @throws {Error} If the file cannot be read.
|
|
* @private
|
|
*/
|
|
function loadLegacyConfigFile(filePath) {
|
|
debug$2(`Loading legacy config file: ${filePath}`);
|
|
|
|
// lazy load YAML to improve performance when not used
|
|
const yaml = require$1("js-yaml");
|
|
|
|
try {
|
|
return yaml.load(stripComments__default["default"](readFile(filePath))) || /* istanbul ignore next */ {};
|
|
} catch (e) {
|
|
debug$2("Error reading YAML file: %s\n%o", filePath, e);
|
|
e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads a JavaScript configuration from a file.
|
|
* @param {string} filePath The filename to load.
|
|
* @returns {ConfigData} The configuration object from the file.
|
|
* @throws {Error} If the file cannot be read.
|
|
* @private
|
|
*/
|
|
function loadJSConfigFile(filePath) {
|
|
debug$2(`Loading JS config file: ${filePath}`);
|
|
try {
|
|
return importFresh__default["default"](filePath);
|
|
} catch (e) {
|
|
debug$2(`Error reading JavaScript file: ${filePath}`);
|
|
e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads a configuration from a package.json file.
|
|
* @param {string} filePath The filename to load.
|
|
* @returns {ConfigData} The configuration object from the file.
|
|
* @throws {Error} If the file cannot be read.
|
|
* @private
|
|
*/
|
|
function loadPackageJSONConfigFile(filePath) {
|
|
debug$2(`Loading package.json config file: ${filePath}`);
|
|
try {
|
|
const packageData = loadJSONConfigFile(filePath);
|
|
|
|
if (!Object.hasOwnProperty.call(packageData, "eslintConfig")) {
|
|
throw Object.assign(
|
|
new Error("package.json file doesn't have 'eslintConfig' field."),
|
|
{ code: "ESLINT_CONFIG_FIELD_NOT_FOUND" }
|
|
);
|
|
}
|
|
|
|
return packageData.eslintConfig;
|
|
} catch (e) {
|
|
debug$2(`Error reading package.json file: ${filePath}`);
|
|
e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads a `.eslintignore` from a file.
|
|
* @param {string} filePath The filename to load.
|
|
* @returns {string[]} The ignore patterns from the file.
|
|
* @private
|
|
*/
|
|
function loadESLintIgnoreFile(filePath) {
|
|
debug$2(`Loading .eslintignore file: ${filePath}`);
|
|
|
|
try {
|
|
return readFile(filePath)
|
|
.split(/\r?\n/gu)
|
|
.filter(line => line.trim() !== "" && !line.startsWith("#"));
|
|
} catch (e) {
|
|
debug$2(`Error reading .eslintignore file: ${filePath}`);
|
|
e.message = `Cannot read .eslintignore file: ${filePath}\nError: ${e.message}`;
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates an error to notify about a missing config to extend from.
|
|
* @param {string} configName The name of the missing config.
|
|
* @param {string} importerName The name of the config that imported the missing config
|
|
* @param {string} messageTemplate The text template to source error strings from.
|
|
* @returns {Error} The error object to throw
|
|
* @private
|
|
*/
|
|
function configInvalidError(configName, importerName, messageTemplate) {
|
|
return Object.assign(
|
|
new Error(`Failed to load config "${configName}" to extend from.`),
|
|
{
|
|
messageTemplate,
|
|
messageData: { configName, importerName }
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Loads a configuration file regardless of the source. Inspects the file path
|
|
* to determine the correctly way to load the config file.
|
|
* @param {string} filePath The path to the configuration.
|
|
* @returns {ConfigData|null} The configuration information.
|
|
* @private
|
|
*/
|
|
function loadConfigFile(filePath) {
|
|
switch (path__default["default"].extname(filePath)) {
|
|
case ".js":
|
|
case ".cjs":
|
|
return loadJSConfigFile(filePath);
|
|
|
|
case ".json":
|
|
if (path__default["default"].basename(filePath) === "package.json") {
|
|
return loadPackageJSONConfigFile(filePath);
|
|
}
|
|
return loadJSONConfigFile(filePath);
|
|
|
|
case ".yaml":
|
|
case ".yml":
|
|
return loadYAMLConfigFile(filePath);
|
|
|
|
default:
|
|
return loadLegacyConfigFile(filePath);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write debug log.
|
|
* @param {string} request The requested module name.
|
|
* @param {string} relativeTo The file path to resolve the request relative to.
|
|
* @param {string} filePath The resolved file path.
|
|
* @returns {void}
|
|
*/
|
|
function writeDebugLogForLoading(request, relativeTo, filePath) {
|
|
/* istanbul ignore next */
|
|
if (debug$2.enabled) {
|
|
let nameAndVersion = null;
|
|
|
|
try {
|
|
const packageJsonPath = resolve(
|
|
`${request}/package.json`,
|
|
relativeTo
|
|
);
|
|
const { version = "unknown" } = require$1(packageJsonPath);
|
|
|
|
nameAndVersion = `${request}@${version}`;
|
|
} catch (error) {
|
|
debug$2("package.json was not found:", error.message);
|
|
nameAndVersion = request;
|
|
}
|
|
|
|
debug$2("Loaded: %s (%s)", nameAndVersion, filePath);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new context with default values.
|
|
* @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, resolvePluginsRelativeTo },
|
|
providedType,
|
|
providedName,
|
|
providedFilePath,
|
|
providedMatchBasePath
|
|
) {
|
|
const filePath = providedFilePath
|
|
? path__default["default"].resolve(cwd, providedFilePath)
|
|
: "";
|
|
const matchBasePath =
|
|
(providedMatchBasePath && path__default["default"].resolve(cwd, providedMatchBasePath)) ||
|
|
(filePath && path__default["default"].dirname(filePath)) ||
|
|
cwd;
|
|
const name =
|
|
providedName ||
|
|
(filePath && path__default["default"].relative(cwd, filePath)) ||
|
|
"";
|
|
const pluginBasePath =
|
|
resolvePluginsRelativeTo ||
|
|
(filePath && path__default["default"].dirname(filePath)) ||
|
|
cwd;
|
|
const type = providedType || "config";
|
|
|
|
return { filePath, matchBasePath, name, pluginBasePath, type };
|
|
}
|
|
|
|
/**
|
|
* Normalize a given plugin.
|
|
* - Ensure the object to have four properties: configs, environments, processors, and rules.
|
|
* - Ensure the object to not have other properties.
|
|
* @param {Plugin} plugin The plugin to normalize.
|
|
* @returns {Plugin} The normalized plugin.
|
|
*/
|
|
function normalizePlugin(plugin) {
|
|
return {
|
|
configs: plugin.configs || {},
|
|
environments: plugin.environments || {},
|
|
processors: plugin.processors || {},
|
|
rules: plugin.rules || {}
|
|
};
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Public Interface
|
|
//------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* The factory of `ConfigArray` objects.
|
|
*/
|
|
class ConfigArrayFactory {
|
|
|
|
/**
|
|
* Initialize this instance.
|
|
* @param {ConfigArrayFactoryOptions} [options] The map for additional plugins.
|
|
*/
|
|
constructor({
|
|
additionalPluginPool = new Map(),
|
|
cwd = process.cwd(),
|
|
resolvePluginsRelativeTo,
|
|
builtInRules,
|
|
resolver = ModuleResolver,
|
|
eslintAllPath,
|
|
getEslintAllConfig,
|
|
eslintRecommendedPath,
|
|
getEslintRecommendedConfig
|
|
} = {}) {
|
|
internalSlotsMap$1.set(this, {
|
|
additionalPluginPool,
|
|
cwd,
|
|
resolvePluginsRelativeTo:
|
|
resolvePluginsRelativeTo &&
|
|
path__default["default"].resolve(cwd, resolvePluginsRelativeTo),
|
|
builtInRules,
|
|
resolver,
|
|
eslintAllPath,
|
|
getEslintAllConfig,
|
|
eslintRecommendedPath,
|
|
getEslintRecommendedConfig
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create `ConfigArray` instance from a config data.
|
|
* @param {ConfigData|null} configData The config data to create.
|
|
* @param {Object} [options] The options.
|
|
* @param {string} [options.basePath] The base path to resolve relative paths in `overrides[].files`, `overrides[].excludedFiles`, and `ignorePatterns`.
|
|
* @param {string} [options.filePath] The path to this config data.
|
|
* @param {string} [options.name] The config name.
|
|
* @returns {ConfigArray} Loaded config.
|
|
*/
|
|
create(configData, { basePath, filePath, name } = {}) {
|
|
if (!configData) {
|
|
return new ConfigArray();
|
|
}
|
|
|
|
const slots = internalSlotsMap$1.get(this);
|
|
const ctx = createContext(slots, "config", name, filePath, basePath);
|
|
const elements = this._normalizeConfigData(configData, ctx);
|
|
|
|
return new ConfigArray(...elements);
|
|
}
|
|
|
|
/**
|
|
* Load a config file.
|
|
* @param {string} filePath The path to a config file.
|
|
* @param {Object} [options] The options.
|
|
* @param {string} [options.basePath] The base path to resolve relative paths in `overrides[].files`, `overrides[].excludedFiles`, and `ignorePatterns`.
|
|
* @param {string} [options.name] The config name.
|
|
* @returns {ConfigArray} Loaded config.
|
|
*/
|
|
loadFile(filePath, { basePath, name } = {}) {
|
|
const slots = internalSlotsMap$1.get(this);
|
|
const ctx = createContext(slots, "config", name, filePath, basePath);
|
|
|
|
return new ConfigArray(...this._loadConfigData(ctx));
|
|
}
|
|
|
|
/**
|
|
* Load the config file on a given directory if exists.
|
|
* @param {string} directoryPath The path to a directory.
|
|
* @param {Object} [options] The options.
|
|
* @param {string} [options.basePath] The base path to resolve relative paths in `overrides[].files`, `overrides[].excludedFiles`, and `ignorePatterns`.
|
|
* @param {string} [options.name] The config name.
|
|
* @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist.
|
|
*/
|
|
loadInDirectory(directoryPath, { basePath, name } = {}) {
|
|
const slots = internalSlotsMap$1.get(this);
|
|
|
|
for (const filename of configFilenames) {
|
|
const ctx = createContext(
|
|
slots,
|
|
"config",
|
|
name,
|
|
path__default["default"].join(directoryPath, filename),
|
|
basePath
|
|
);
|
|
|
|
if (fs__default["default"].existsSync(ctx.filePath) && fs__default["default"].statSync(ctx.filePath).isFile()) {
|
|
let configData;
|
|
|
|
try {
|
|
configData = loadConfigFile(ctx.filePath);
|
|
} catch (error) {
|
|
if (!error || error.code !== "ESLINT_CONFIG_FIELD_NOT_FOUND") {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
if (configData) {
|
|
debug$2(`Config file found: ${ctx.filePath}`);
|
|
return new ConfigArray(
|
|
...this._normalizeConfigData(configData, ctx)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
debug$2(`Config file not found on ${directoryPath}`);
|
|
return new ConfigArray();
|
|
}
|
|
|
|
/**
|
|
* Check if a config file on a given directory exists or not.
|
|
* @param {string} directoryPath The path to a directory.
|
|
* @returns {string | null} The path to the found config file. If not found then null.
|
|
*/
|
|
static getPathToConfigFileInDirectory(directoryPath) {
|
|
for (const filename of configFilenames) {
|
|
const filePath = path__default["default"].join(directoryPath, filename);
|
|
|
|
if (fs__default["default"].existsSync(filePath)) {
|
|
if (filename === "package.json") {
|
|
try {
|
|
loadPackageJSONConfigFile(filePath);
|
|
return filePath;
|
|
} catch { /* ignore */ }
|
|
} else {
|
|
return filePath;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Load `.eslintignore` file.
|
|
* @param {string} filePath The path to a `.eslintignore` file to load.
|
|
* @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist.
|
|
*/
|
|
loadESLintIgnore(filePath) {
|
|
const slots = internalSlotsMap$1.get(this);
|
|
const ctx = createContext(
|
|
slots,
|
|
"ignore",
|
|
void 0,
|
|
filePath,
|
|
slots.cwd
|
|
);
|
|
const ignorePatterns = loadESLintIgnoreFile(ctx.filePath);
|
|
|
|
return new ConfigArray(
|
|
...this._normalizeESLintIgnoreData(ignorePatterns, ctx)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Load `.eslintignore` file in the current working directory.
|
|
* @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist.
|
|
*/
|
|
loadDefaultESLintIgnore() {
|
|
const slots = internalSlotsMap$1.get(this);
|
|
const eslintIgnorePath = path__default["default"].resolve(slots.cwd, ".eslintignore");
|
|
const packageJsonPath = path__default["default"].resolve(slots.cwd, "package.json");
|
|
|
|
if (fs__default["default"].existsSync(eslintIgnorePath)) {
|
|
return this.loadESLintIgnore(eslintIgnorePath);
|
|
}
|
|
if (fs__default["default"].existsSync(packageJsonPath)) {
|
|
const data = loadJSONConfigFile(packageJsonPath);
|
|
|
|
if (Object.hasOwnProperty.call(data, "eslintIgnore")) {
|
|
if (!Array.isArray(data.eslintIgnore)) {
|
|
throw new Error("Package.json eslintIgnore property requires an array of paths");
|
|
}
|
|
const ctx = createContext(
|
|
slots,
|
|
"ignore",
|
|
"eslintIgnore in package.json",
|
|
packageJsonPath,
|
|
slots.cwd
|
|
);
|
|
|
|
return new ConfigArray(
|
|
...this._normalizeESLintIgnoreData(data.eslintIgnore, ctx)
|
|
);
|
|
}
|
|
}
|
|
|
|
return new ConfigArray();
|
|
}
|
|
|
|
/**
|
|
* Load a given config file.
|
|
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
|
|
* @returns {IterableIterator<ConfigArrayElement>} Loaded config.
|
|
* @private
|
|
*/
|
|
_loadConfigData(ctx) {
|
|
return this._normalizeConfigData(loadConfigFile(ctx.filePath), ctx);
|
|
}
|
|
|
|
/**
|
|
* Normalize a given `.eslintignore` data to config array elements.
|
|
* @param {string[]} ignorePatterns The patterns to ignore files.
|
|
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
|
|
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
|
|
* @private
|
|
*/
|
|
*_normalizeESLintIgnoreData(ignorePatterns, ctx) {
|
|
const elements = this._normalizeObjectConfigData(
|
|
{ ignorePatterns },
|
|
ctx
|
|
);
|
|
|
|
// Set `ignorePattern.loose` flag for backward compatibility.
|
|
for (const element of elements) {
|
|
if (element.ignorePattern) {
|
|
element.ignorePattern.loose = true;
|
|
}
|
|
yield element;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Normalize a given config to an array.
|
|
* @param {ConfigData} configData The config data to normalize.
|
|
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
|
|
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
|
|
* @private
|
|
*/
|
|
_normalizeConfigData(configData, ctx) {
|
|
const validator = new ConfigValidator();
|
|
|
|
validator.validateConfigSchema(configData, ctx.name || ctx.filePath);
|
|
return this._normalizeObjectConfigData(configData, ctx);
|
|
}
|
|
|
|
/**
|
|
* Normalize a given config to an array.
|
|
* @param {ConfigData|OverrideConfigData} configData The config data to normalize.
|
|
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
|
|
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
|
|
* @private
|
|
*/
|
|
*_normalizeObjectConfigData(configData, ctx) {
|
|
const { files, excludedFiles, ...configBody } = configData;
|
|
const criteria = OverrideTester.create(
|
|
files,
|
|
excludedFiles,
|
|
ctx.matchBasePath
|
|
);
|
|
const elements = this._normalizeObjectConfigDataBody(configBody, ctx);
|
|
|
|
// Apply the criteria to every element.
|
|
for (const element of elements) {
|
|
|
|
/*
|
|
* Merge the criteria.
|
|
* This is for the `overrides` entries that came from the
|
|
* configurations of `overrides[].extends`.
|
|
*/
|
|
element.criteria = OverrideTester.and(criteria, element.criteria);
|
|
|
|
/*
|
|
* Remove `root` property to ignore `root` settings which came from
|
|
* `extends` in `overrides`.
|
|
*/
|
|
if (element.criteria) {
|
|
element.root = void 0;
|
|
}
|
|
|
|
yield element;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Normalize a given config to an array.
|
|
* @param {ConfigData} configData The config data to normalize.
|
|
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
|
|
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
|
|
* @private
|
|
*/
|
|
*_normalizeObjectConfigDataBody(
|
|
{
|
|
env,
|
|
extends: extend,
|
|
globals,
|
|
ignorePatterns,
|
|
noInlineConfig,
|
|
parser: parserName,
|
|
parserOptions,
|
|
plugins: pluginList,
|
|
processor,
|
|
reportUnusedDisableDirectives,
|
|
root,
|
|
rules,
|
|
settings,
|
|
overrides: overrideList = []
|
|
},
|
|
ctx
|
|
) {
|
|
const extendList = Array.isArray(extend) ? extend : [extend];
|
|
const ignorePattern = ignorePatterns && new IgnorePattern(
|
|
Array.isArray(ignorePatterns) ? ignorePatterns : [ignorePatterns],
|
|
ctx.matchBasePath
|
|
);
|
|
|
|
// Flatten `extends`.
|
|
for (const extendName of extendList.filter(Boolean)) {
|
|
yield* this._loadExtends(extendName, ctx);
|
|
}
|
|
|
|
// Load parser & plugins.
|
|
const parser = parserName && this._loadParser(parserName, ctx);
|
|
const plugins = pluginList && this._loadPlugins(pluginList, ctx);
|
|
|
|
// Yield pseudo config data for file extension processors.
|
|
if (plugins) {
|
|
yield* this._takeFileExtensionProcessors(plugins, ctx);
|
|
}
|
|
|
|
// Yield the config data except `extends` and `overrides`.
|
|
yield {
|
|
|
|
// Debug information.
|
|
type: ctx.type,
|
|
name: ctx.name,
|
|
filePath: ctx.filePath,
|
|
|
|
// Config data.
|
|
criteria: null,
|
|
env,
|
|
globals,
|
|
ignorePattern,
|
|
noInlineConfig,
|
|
parser,
|
|
parserOptions,
|
|
plugins,
|
|
processor,
|
|
reportUnusedDisableDirectives,
|
|
root,
|
|
rules,
|
|
settings
|
|
};
|
|
|
|
// Flatten `overries`.
|
|
for (let i = 0; i < overrideList.length; ++i) {
|
|
yield* this._normalizeObjectConfigData(
|
|
overrideList[i],
|
|
{ ...ctx, name: `${ctx.name}#overrides[${i}]` }
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load configs of an element in `extends`.
|
|
* @param {string} extendName The name of a base config.
|
|
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
|
|
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
|
|
* @private
|
|
*/
|
|
_loadExtends(extendName, ctx) {
|
|
debug$2("Loading {extends:%j} relative to %s", extendName, ctx.filePath);
|
|
try {
|
|
if (extendName.startsWith("eslint:")) {
|
|
return this._loadExtendedBuiltInConfig(extendName, ctx);
|
|
}
|
|
if (extendName.startsWith("plugin:")) {
|
|
return this._loadExtendedPluginConfig(extendName, ctx);
|
|
}
|
|
return this._loadExtendedShareableConfig(extendName, ctx);
|
|
} catch (error) {
|
|
error.message += `\nReferenced from: ${ctx.filePath || ctx.name}`;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load configs of an element in `extends`.
|
|
* @param {string} extendName The name of a base config.
|
|
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
|
|
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
|
|
* @private
|
|
*/
|
|
_loadExtendedBuiltInConfig(extendName, ctx) {
|
|
const {
|
|
eslintAllPath,
|
|
getEslintAllConfig,
|
|
eslintRecommendedPath,
|
|
getEslintRecommendedConfig
|
|
} = internalSlotsMap$1.get(this);
|
|
|
|
if (extendName === "eslint:recommended") {
|
|
const name = `${ctx.name} » ${extendName}`;
|
|
|
|
if (getEslintRecommendedConfig) {
|
|
if (typeof getEslintRecommendedConfig !== "function") {
|
|
throw new Error(`getEslintRecommendedConfig must be a function instead of '${getEslintRecommendedConfig}'`);
|
|
}
|
|
return this._normalizeConfigData(getEslintRecommendedConfig(), { ...ctx, name, filePath: "" });
|
|
}
|
|
return this._loadConfigData({
|
|
...ctx,
|
|
name,
|
|
filePath: eslintRecommendedPath
|
|
});
|
|
}
|
|
if (extendName === "eslint:all") {
|
|
const name = `${ctx.name} » ${extendName}`;
|
|
|
|
if (getEslintAllConfig) {
|
|
if (typeof getEslintAllConfig !== "function") {
|
|
throw new Error(`getEslintAllConfig must be a function instead of '${getEslintAllConfig}'`);
|
|
}
|
|
return this._normalizeConfigData(getEslintAllConfig(), { ...ctx, name, filePath: "" });
|
|
}
|
|
return this._loadConfigData({
|
|
...ctx,
|
|
name,
|
|
filePath: eslintAllPath
|
|
});
|
|
}
|
|
|
|
throw configInvalidError(extendName, ctx.name, "extend-config-missing");
|
|
}
|
|
|
|
/**
|
|
* Load configs of an element in `extends`.
|
|
* @param {string} extendName The name of a base config.
|
|
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
|
|
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
|
|
* @private
|
|
*/
|
|
_loadExtendedPluginConfig(extendName, ctx) {
|
|
const slashIndex = extendName.lastIndexOf("/");
|
|
|
|
if (slashIndex === -1) {
|
|
throw configInvalidError(extendName, ctx.filePath, "plugin-invalid");
|
|
}
|
|
|
|
const pluginName = extendName.slice("plugin:".length, slashIndex);
|
|
const configName = extendName.slice(slashIndex + 1);
|
|
|
|
if (isFilePath(pluginName)) {
|
|
throw new Error("'extends' cannot use a file path for plugins.");
|
|
}
|
|
|
|
const plugin = this._loadPlugin(pluginName, ctx);
|
|
const configData =
|
|
plugin.definition &&
|
|
plugin.definition.configs[configName];
|
|
|
|
if (configData) {
|
|
return this._normalizeConfigData(configData, {
|
|
...ctx,
|
|
filePath: plugin.filePath || ctx.filePath,
|
|
name: `${ctx.name} » plugin:${plugin.id}/${configName}`
|
|
});
|
|
}
|
|
|
|
throw plugin.error || configInvalidError(extendName, ctx.filePath, "extend-config-missing");
|
|
}
|
|
|
|
/**
|
|
* Load configs of an element in `extends`.
|
|
* @param {string} extendName The name of a base config.
|
|
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
|
|
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
|
|
* @private
|
|
*/
|
|
_loadExtendedShareableConfig(extendName, ctx) {
|
|
const { cwd, resolver } = internalSlotsMap$1.get(this);
|
|
const relativeTo = ctx.filePath || path__default["default"].join(cwd, "__placeholder__.js");
|
|
let request;
|
|
|
|
if (isFilePath(extendName)) {
|
|
request = extendName;
|
|
} else if (extendName.startsWith(".")) {
|
|
request = `./${extendName}`; // For backward compatibility. A ton of tests depended on this behavior.
|
|
} else {
|
|
request = normalizePackageName(
|
|
extendName,
|
|
"eslint-config"
|
|
);
|
|
}
|
|
|
|
let filePath;
|
|
|
|
try {
|
|
filePath = resolver.resolve(request, relativeTo);
|
|
} catch (error) {
|
|
/* istanbul ignore else */
|
|
if (error && error.code === "MODULE_NOT_FOUND") {
|
|
throw configInvalidError(extendName, ctx.filePath, "extend-config-missing");
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
writeDebugLogForLoading(request, relativeTo, filePath);
|
|
return this._loadConfigData({
|
|
...ctx,
|
|
filePath,
|
|
name: `${ctx.name} » ${request}`
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Load given plugins.
|
|
* @param {string[]} names The plugin names to load.
|
|
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
|
|
* @returns {Record<string,DependentPlugin>} The loaded parser.
|
|
* @private
|
|
*/
|
|
_loadPlugins(names, ctx) {
|
|
return names.reduce((map, name) => {
|
|
if (isFilePath(name)) {
|
|
throw new Error("Plugins array cannot includes file paths.");
|
|
}
|
|
const plugin = this._loadPlugin(name, ctx);
|
|
|
|
map[plugin.id] = plugin;
|
|
|
|
return map;
|
|
}, {});
|
|
}
|
|
|
|
/**
|
|
* Load a given parser.
|
|
* @param {string} nameOrPath The package name or the path to a parser file.
|
|
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
|
|
* @returns {DependentParser} The loaded parser.
|
|
*/
|
|
_loadParser(nameOrPath, ctx) {
|
|
debug$2("Loading parser %j from %s", nameOrPath, ctx.filePath);
|
|
|
|
const { cwd, resolver } = internalSlotsMap$1.get(this);
|
|
const relativeTo = ctx.filePath || path__default["default"].join(cwd, "__placeholder__.js");
|
|
|
|
try {
|
|
const filePath = resolver.resolve(nameOrPath, relativeTo);
|
|
|
|
writeDebugLogForLoading(nameOrPath, relativeTo, filePath);
|
|
|
|
return new ConfigDependency({
|
|
definition: require$1(filePath),
|
|
filePath,
|
|
id: nameOrPath,
|
|
importerName: ctx.name,
|
|
importerPath: ctx.filePath
|
|
});
|
|
} catch (error) {
|
|
|
|
// If the parser name is "espree", load the espree of ESLint.
|
|
if (nameOrPath === "espree") {
|
|
debug$2("Fallback espree.");
|
|
return new ConfigDependency({
|
|
definition: require$1("espree"),
|
|
filePath: require$1.resolve("espree"),
|
|
id: nameOrPath,
|
|
importerName: ctx.name,
|
|
importerPath: ctx.filePath
|
|
});
|
|
}
|
|
|
|
debug$2("Failed to load parser '%s' declared in '%s'.", nameOrPath, ctx.name);
|
|
error.message = `Failed to load parser '${nameOrPath}' declared in '${ctx.name}': ${error.message}`;
|
|
|
|
return new ConfigDependency({
|
|
error,
|
|
id: nameOrPath,
|
|
importerName: ctx.name,
|
|
importerPath: ctx.filePath
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load a given plugin.
|
|
* @param {string} name The plugin name to load.
|
|
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
|
|
* @returns {DependentPlugin} The loaded plugin.
|
|
* @private
|
|
*/
|
|
_loadPlugin(name, ctx) {
|
|
debug$2("Loading plugin %j from %s", name, ctx.filePath);
|
|
|
|
const { additionalPluginPool, resolver } = internalSlotsMap$1.get(this);
|
|
const request = normalizePackageName(name, "eslint-plugin");
|
|
const id = getShorthandName(request, "eslint-plugin");
|
|
const relativeTo = path__default["default"].join(ctx.pluginBasePath, "__placeholder__.js");
|
|
|
|
if (name.match(/\s+/u)) {
|
|
const error = Object.assign(
|
|
new Error(`Whitespace found in plugin name '${name}'`),
|
|
{
|
|
messageTemplate: "whitespace-found",
|
|
messageData: { pluginName: request }
|
|
}
|
|
);
|
|
|
|
return new ConfigDependency({
|
|
error,
|
|
id,
|
|
importerName: ctx.name,
|
|
importerPath: ctx.filePath
|
|
});
|
|
}
|
|
|
|
// Check for additional pool.
|
|
const plugin =
|
|
additionalPluginPool.get(request) ||
|
|
additionalPluginPool.get(id);
|
|
|
|
if (plugin) {
|
|
return new ConfigDependency({
|
|
definition: normalizePlugin(plugin),
|
|
filePath: "", // It's unknown where the plugin came from.
|
|
id,
|
|
importerName: ctx.name,
|
|
importerPath: ctx.filePath
|
|
});
|
|
}
|
|
|
|
let filePath;
|
|
let error;
|
|
|
|
try {
|
|
filePath = resolver.resolve(request, relativeTo);
|
|
} catch (resolveError) {
|
|
error = resolveError;
|
|
/* istanbul ignore else */
|
|
if (error && error.code === "MODULE_NOT_FOUND") {
|
|
error.messageTemplate = "plugin-missing";
|
|
error.messageData = {
|
|
pluginName: request,
|
|
resolvePluginsRelativeTo: ctx.pluginBasePath,
|
|
importerName: ctx.name
|
|
};
|
|
}
|
|
}
|
|
|
|
if (filePath) {
|
|
try {
|
|
writeDebugLogForLoading(request, relativeTo, filePath);
|
|
|
|
const startTime = Date.now();
|
|
const pluginDefinition = require$1(filePath);
|
|
|
|
debug$2(`Plugin ${filePath} loaded in: ${Date.now() - startTime}ms`);
|
|
|
|
return new ConfigDependency({
|
|
definition: normalizePlugin(pluginDefinition),
|
|
filePath,
|
|
id,
|
|
importerName: ctx.name,
|
|
importerPath: ctx.filePath
|
|
});
|
|
} catch (loadError) {
|
|
error = loadError;
|
|
}
|
|
}
|
|
|
|
debug$2("Failed to load plugin '%s' declared in '%s'.", name, ctx.name);
|
|
error.message = `Failed to load plugin '${name}' declared in '${ctx.name}': ${error.message}`;
|
|
return new ConfigDependency({
|
|
error,
|
|
id,
|
|
importerName: ctx.name,
|
|
importerPath: ctx.filePath
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Take file expression processors as config array elements.
|
|
* @param {Record<string,DependentPlugin>} plugins The plugin definitions.
|
|
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
|
|
* @returns {IterableIterator<ConfigArrayElement>} The config array elements of file expression processors.
|
|
* @private
|
|
*/
|
|
*_takeFileExtensionProcessors(plugins, ctx) {
|
|
for (const pluginId of Object.keys(plugins)) {
|
|
const processors =
|
|
plugins[pluginId] &&
|
|
plugins[pluginId].definition &&
|
|
plugins[pluginId].definition.processors;
|
|
|
|
if (!processors) {
|
|
continue;
|
|
}
|
|
|
|
for (const processorId of Object.keys(processors)) {
|
|
if (processorId.startsWith(".")) {
|
|
yield* this._normalizeObjectConfigData(
|
|
{
|
|
files: [`*${processorId}`],
|
|
processor: `${pluginId}/${processorId}`
|
|
},
|
|
{
|
|
...ctx,
|
|
type: "implicit-processor",
|
|
name: `${ctx.name}#processors["${pluginId}/${processorId}"]`
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @fileoverview `CascadingConfigArrayFactory` class.
|
|
*
|
|
* `CascadingConfigArrayFactory` class has a responsibility:
|
|
*
|
|
* 1. Handles cascading of config files.
|
|
*
|
|
* It provides two methods:
|
|
*
|
|
* - `getConfigArrayForFile(filePath)`
|
|
* Get the corresponded configuration of a given file. This method doesn't
|
|
* throw even if the given file didn't exist.
|
|
* - `clearCache()`
|
|
* Clear the internal cache. You have to call this method when
|
|
* `additionalPluginPool` was updated if `baseConfig` or `cliConfig` depends
|
|
* on the additional plugins. (`CLIEngine#addPlugin()` method calls this.)
|
|
*
|
|
* @author Toru Nagashima <https://github.com/mysticatea>
|
|
*/
|
|
|
|
const debug$1 = debugOrig__default["default"]("eslintrc:cascading-config-array-factory");
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Helpers
|
|
//------------------------------------------------------------------------------
|
|
|
|
// Define types for VSCode IntelliSense.
|
|
/** @typedef {import("./shared/types").ConfigData} ConfigData */
|
|
/** @typedef {import("./shared/types").Parser} Parser */
|
|
/** @typedef {import("./shared/types").Plugin} Plugin */
|
|
/** @typedef {import("./shared/types").Rule} Rule */
|
|
/** @typedef {ReturnType<ConfigArrayFactory["create"]>} ConfigArray */
|
|
|
|
/**
|
|
* @typedef {Object} CascadingConfigArrayFactoryOptions
|
|
* @property {Map<string,Plugin>} [additionalPluginPool] The map for additional plugins.
|
|
* @property {ConfigData} [baseConfig] The config by `baseConfig` option.
|
|
* @property {ConfigData} [cliConfig] The config by CLI options (`--env`, `--global`, `--ignore-pattern`, `--parser`, `--parser-options`, `--plugin`, and `--rule`). CLI options overwrite the setting in config files.
|
|
* @property {string} [cwd] The base directory to start lookup.
|
|
* @property {string} [ignorePath] The path to the alternative file of `.eslintignore`.
|
|
* @property {string[]} [rulePaths] The value of `--rulesdir` option.
|
|
* @property {string} [specificConfigPath] The value of `--config` option.
|
|
* @property {boolean} [useEslintrc] if `false` then it doesn't load config files.
|
|
* @property {Function} loadRules The function to use to load rules.
|
|
* @property {Map<string,Rule>} builtInRules The rules that are built in to ESLint.
|
|
* @property {Object} [resolver=ModuleResolver] The module resolver object.
|
|
* @property {string} eslintAllPath The path to the definitions for eslint:all.
|
|
* @property {Function} getEslintAllConfig Returns the config data for eslint:all.
|
|
* @property {string} eslintRecommendedPath The path to the definitions for eslint:recommended.
|
|
* @property {Function} getEslintRecommendedConfig Returns the config data for eslint:recommended.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} CascadingConfigArrayFactoryInternalSlots
|
|
* @property {ConfigArray} baseConfigArray The config array of `baseConfig` option.
|
|
* @property {ConfigData} baseConfigData The config data of `baseConfig` option. This is used to reset `baseConfigArray`.
|
|
* @property {ConfigArray} cliConfigArray The config array of CLI options.
|
|
* @property {ConfigData} cliConfigData The config data of CLI options. This is used to reset `cliConfigArray`.
|
|
* @property {ConfigArrayFactory} configArrayFactory The factory for config arrays.
|
|
* @property {Map<string, ConfigArray>} configCache The cache from directory paths to config arrays.
|
|
* @property {string} cwd The base directory to start lookup.
|
|
* @property {WeakMap<ConfigArray, ConfigArray>} finalizeCache The cache from config arrays to finalized config arrays.
|
|
* @property {string} [ignorePath] The path to the alternative file of `.eslintignore`.
|
|
* @property {string[]|null} rulePaths The value of `--rulesdir` option. This is used to reset `baseConfigArray`.
|
|
* @property {string|null} specificConfigPath The value of `--config` option. This is used to reset `cliConfigArray`.
|
|
* @property {boolean} useEslintrc if `false` then it doesn't load config files.
|
|
* @property {Function} loadRules The function to use to load rules.
|
|
* @property {Map<string,Rule>} builtInRules The rules that are built in to ESLint.
|
|
* @property {Object} [resolver=ModuleResolver] The module resolver object.
|
|
* @property {string} eslintAllPath The path to the definitions for eslint:all.
|
|
* @property {Function} getEslintAllConfig Returns the config data for eslint:all.
|
|
* @property {string} eslintRecommendedPath The path to the definitions for eslint:recommended.
|
|
* @property {Function} getEslintRecommendedConfig Returns the config data for eslint:recommended.
|
|
*/
|
|
|
|
/** @type {WeakMap<CascadingConfigArrayFactory, CascadingConfigArrayFactoryInternalSlots>} */
|
|
const internalSlotsMap = new WeakMap();
|
|
|
|
/**
|
|
* Create the config array from `baseConfig` and `rulePaths`.
|
|
* @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
|
|
* @returns {ConfigArray} The config array of the base configs.
|
|
*/
|
|
function createBaseConfigArray({
|
|
configArrayFactory,
|
|
baseConfigData,
|
|
rulePaths,
|
|
cwd,
|
|
loadRules
|
|
}) {
|
|
const baseConfigArray = configArrayFactory.create(
|
|
baseConfigData,
|
|
{ name: "BaseConfig" }
|
|
);
|
|
|
|
/*
|
|
* Create the config array element for the default ignore patterns.
|
|
* This element has `ignorePattern` property that ignores the default
|
|
* patterns in the current working directory.
|
|
*/
|
|
baseConfigArray.unshift(configArrayFactory.create(
|
|
{ ignorePatterns: IgnorePattern.DefaultPatterns },
|
|
{ name: "DefaultIgnorePattern" }
|
|
)[0]);
|
|
|
|
/*
|
|
* Load rules `--rulesdir` option as a pseudo plugin.
|
|
* Use a pseudo plugin to define rules of `--rulesdir`, so we can validate
|
|
* the rule's options with only information in the config array.
|
|
*/
|
|
if (rulePaths && rulePaths.length > 0) {
|
|
baseConfigArray.push({
|
|
type: "config",
|
|
name: "--rulesdir",
|
|
filePath: "",
|
|
plugins: {
|
|
"": new ConfigDependency({
|
|
definition: {
|
|
rules: rulePaths.reduce(
|
|
(map, rulesPath) => Object.assign(
|
|
map,
|
|
loadRules(rulesPath, cwd)
|
|
),
|
|
{}
|
|
)
|
|
},
|
|
filePath: "",
|
|
id: "",
|
|
importerName: "--rulesdir",
|
|
importerPath: ""
|
|
})
|
|
}
|
|
});
|
|
}
|
|
|
|
return baseConfigArray;
|
|
}
|
|
|
|
/**
|
|
* Create the config array from CLI options.
|
|
* @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
|
|
* @returns {ConfigArray} The config array of the base configs.
|
|
*/
|
|
function createCLIConfigArray({
|
|
cliConfigData,
|
|
configArrayFactory,
|
|
cwd,
|
|
ignorePath,
|
|
specificConfigPath
|
|
}) {
|
|
const cliConfigArray = configArrayFactory.create(
|
|
cliConfigData,
|
|
{ name: "CLIOptions" }
|
|
);
|
|
|
|
cliConfigArray.unshift(
|
|
...(ignorePath
|
|
? configArrayFactory.loadESLintIgnore(ignorePath)
|
|
: configArrayFactory.loadDefaultESLintIgnore())
|
|
);
|
|
|
|
if (specificConfigPath) {
|
|
cliConfigArray.unshift(
|
|
...configArrayFactory.loadFile(
|
|
specificConfigPath,
|
|
{ name: "--config", basePath: cwd }
|
|
)
|
|
);
|
|
}
|
|
|
|
return cliConfigArray;
|
|
}
|
|
|
|
/**
|
|
* The error type when there are files matched by a glob, but all of them have been ignored.
|
|
*/
|
|
class ConfigurationNotFoundError extends Error {
|
|
|
|
// eslint-disable-next-line jsdoc/require-description
|
|
/**
|
|
* @param {string} directoryPath The directory path.
|
|
*/
|
|
constructor(directoryPath) {
|
|
super(`No ESLint configuration found in ${directoryPath}.`);
|
|
this.messageTemplate = "no-config-found";
|
|
this.messageData = { directoryPath };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This class provides the functionality that enumerates every file which is
|
|
* matched by given glob patterns and that configuration.
|
|
*/
|
|
class CascadingConfigArrayFactory {
|
|
|
|
/**
|
|
* Initialize this enumerator.
|
|
* @param {CascadingConfigArrayFactoryOptions} options The options.
|
|
*/
|
|
constructor({
|
|
additionalPluginPool = new Map(),
|
|
baseConfig: baseConfigData = null,
|
|
cliConfig: cliConfigData = null,
|
|
cwd = process.cwd(),
|
|
ignorePath,
|
|
resolvePluginsRelativeTo,
|
|
rulePaths = [],
|
|
specificConfigPath = null,
|
|
useEslintrc = true,
|
|
builtInRules = new Map(),
|
|
loadRules,
|
|
resolver,
|
|
eslintRecommendedPath,
|
|
getEslintRecommendedConfig,
|
|
eslintAllPath,
|
|
getEslintAllConfig
|
|
} = {}) {
|
|
const configArrayFactory = new ConfigArrayFactory({
|
|
additionalPluginPool,
|
|
cwd,
|
|
resolvePluginsRelativeTo,
|
|
builtInRules,
|
|
resolver,
|
|
eslintRecommendedPath,
|
|
getEslintRecommendedConfig,
|
|
eslintAllPath,
|
|
getEslintAllConfig
|
|
});
|
|
|
|
internalSlotsMap.set(this, {
|
|
baseConfigArray: createBaseConfigArray({
|
|
baseConfigData,
|
|
configArrayFactory,
|
|
cwd,
|
|
rulePaths,
|
|
loadRules,
|
|
resolver
|
|
}),
|
|
baseConfigData,
|
|
cliConfigArray: createCLIConfigArray({
|
|
cliConfigData,
|
|
configArrayFactory,
|
|
cwd,
|
|
ignorePath,
|
|
specificConfigPath
|
|
}),
|
|
cliConfigData,
|
|
configArrayFactory,
|
|
configCache: new Map(),
|
|
cwd,
|
|
finalizeCache: new WeakMap(),
|
|
ignorePath,
|
|
rulePaths,
|
|
specificConfigPath,
|
|
useEslintrc,
|
|
builtInRules,
|
|
loadRules
|
|
});
|
|
}
|
|
|
|
/**
|
|
* The path to the current working directory.
|
|
* This is used by tests.
|
|
* @type {string}
|
|
*/
|
|
get cwd() {
|
|
const { cwd } = internalSlotsMap.get(this);
|
|
|
|
return cwd;
|
|
}
|
|
|
|
/**
|
|
* Get the config array of a given file.
|
|
* If `filePath` was not given, it returns the config which contains only
|
|
* `baseConfigData` and `cliConfigData`.
|
|
* @param {string} [filePath] The file path to a file.
|
|
* @param {Object} [options] The options.
|
|
* @param {boolean} [options.ignoreNotFoundError] If `true` then it doesn't throw `ConfigurationNotFoundError`.
|
|
* @returns {ConfigArray} The config array of the file.
|
|
*/
|
|
getConfigArrayForFile(filePath, { ignoreNotFoundError = false } = {}) {
|
|
const {
|
|
baseConfigArray,
|
|
cliConfigArray,
|
|
cwd
|
|
} = internalSlotsMap.get(this);
|
|
|
|
if (!filePath) {
|
|
return new ConfigArray(...baseConfigArray, ...cliConfigArray);
|
|
}
|
|
|
|
const directoryPath = path__default["default"].dirname(path__default["default"].resolve(cwd, filePath));
|
|
|
|
debug$1(`Load config files for ${directoryPath}.`);
|
|
|
|
return this._finalizeConfigArray(
|
|
this._loadConfigInAncestors(directoryPath),
|
|
directoryPath,
|
|
ignoreNotFoundError
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Set the config data to override all configs.
|
|
* Require to call `clearCache()` method after this method is called.
|
|
* @param {ConfigData} configData The config data to override all configs.
|
|
* @returns {void}
|
|
*/
|
|
setOverrideConfig(configData) {
|
|
const slots = internalSlotsMap.get(this);
|
|
|
|
slots.cliConfigData = configData;
|
|
}
|
|
|
|
/**
|
|
* Clear config cache.
|
|
* @returns {void}
|
|
*/
|
|
clearCache() {
|
|
const slots = internalSlotsMap.get(this);
|
|
|
|
slots.baseConfigArray = createBaseConfigArray(slots);
|
|
slots.cliConfigArray = createCLIConfigArray(slots);
|
|
slots.configCache.clear();
|
|
}
|
|
|
|
/**
|
|
* Load and normalize config files from the ancestor directories.
|
|
* @param {string} directoryPath The path to a leaf directory.
|
|
* @param {boolean} configsExistInSubdirs `true` if configurations exist in subdirectories.
|
|
* @returns {ConfigArray} The loaded config.
|
|
* @private
|
|
*/
|
|
_loadConfigInAncestors(directoryPath, configsExistInSubdirs = false) {
|
|
const {
|
|
baseConfigArray,
|
|
configArrayFactory,
|
|
configCache,
|
|
cwd,
|
|
useEslintrc
|
|
} = internalSlotsMap.get(this);
|
|
|
|
if (!useEslintrc) {
|
|
return baseConfigArray;
|
|
}
|
|
|
|
let configArray = configCache.get(directoryPath);
|
|
|
|
// Hit cache.
|
|
if (configArray) {
|
|
debug$1(`Cache hit: ${directoryPath}.`);
|
|
return configArray;
|
|
}
|
|
debug$1(`No cache found: ${directoryPath}.`);
|
|
|
|
const homePath = os__default["default"].homedir();
|
|
|
|
// Consider this is root.
|
|
if (directoryPath === homePath && cwd !== homePath) {
|
|
debug$1("Stop traversing because of considered root.");
|
|
if (configsExistInSubdirs) {
|
|
const filePath = ConfigArrayFactory.getPathToConfigFileInDirectory(directoryPath);
|
|
|
|
if (filePath) {
|
|
emitDeprecationWarning(
|
|
filePath,
|
|
"ESLINT_PERSONAL_CONFIG_SUPPRESS"
|
|
);
|
|
}
|
|
}
|
|
return this._cacheConfig(directoryPath, baseConfigArray);
|
|
}
|
|
|
|
// Load the config on this directory.
|
|
try {
|
|
configArray = configArrayFactory.loadInDirectory(directoryPath);
|
|
} catch (error) {
|
|
/* istanbul ignore next */
|
|
if (error.code === "EACCES") {
|
|
debug$1("Stop traversing because of 'EACCES' error.");
|
|
return this._cacheConfig(directoryPath, baseConfigArray);
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
if (configArray.length > 0 && configArray.isRoot()) {
|
|
debug$1("Stop traversing because of 'root:true'.");
|
|
configArray.unshift(...baseConfigArray);
|
|
return this._cacheConfig(directoryPath, configArray);
|
|
}
|
|
|
|
// Load from the ancestors and merge it.
|
|
const parentPath = path__default["default"].dirname(directoryPath);
|
|
const parentConfigArray = parentPath && parentPath !== directoryPath
|
|
? this._loadConfigInAncestors(
|
|
parentPath,
|
|
configsExistInSubdirs || configArray.length > 0
|
|
)
|
|
: baseConfigArray;
|
|
|
|
if (configArray.length > 0) {
|
|
configArray.unshift(...parentConfigArray);
|
|
} else {
|
|
configArray = parentConfigArray;
|
|
}
|
|
|
|
// Cache and return.
|
|
return this._cacheConfig(directoryPath, configArray);
|
|
}
|
|
|
|
/**
|
|
* Freeze and cache a given config.
|
|
* @param {string} directoryPath The path to a directory as a cache key.
|
|
* @param {ConfigArray} configArray The config array as a cache value.
|
|
* @returns {ConfigArray} The `configArray` (frozen).
|
|
*/
|
|
_cacheConfig(directoryPath, configArray) {
|
|
const { configCache } = internalSlotsMap.get(this);
|
|
|
|
Object.freeze(configArray);
|
|
configCache.set(directoryPath, configArray);
|
|
|
|
return configArray;
|
|
}
|
|
|
|
/**
|
|
* Finalize a given config array.
|
|
* Concatenate `--config` and other CLI options.
|
|
* @param {ConfigArray} configArray The parent config array.
|
|
* @param {string} directoryPath The path to the leaf directory to find config files.
|
|
* @param {boolean} ignoreNotFoundError If `true` then it doesn't throw `ConfigurationNotFoundError`.
|
|
* @returns {ConfigArray} The loaded config.
|
|
* @private
|
|
*/
|
|
_finalizeConfigArray(configArray, directoryPath, ignoreNotFoundError) {
|
|
const {
|
|
cliConfigArray,
|
|
configArrayFactory,
|
|
finalizeCache,
|
|
useEslintrc,
|
|
builtInRules
|
|
} = internalSlotsMap.get(this);
|
|
|
|
let finalConfigArray = finalizeCache.get(configArray);
|
|
|
|
if (!finalConfigArray) {
|
|
finalConfigArray = configArray;
|
|
|
|
// Load the personal config if there are no regular config files.
|
|
if (
|
|
useEslintrc &&
|
|
configArray.every(c => !c.filePath) &&
|
|
cliConfigArray.every(c => !c.filePath) // `--config` option can be a file.
|
|
) {
|
|
const homePath = os__default["default"].homedir();
|
|
|
|
debug$1("Loading the config file of the home directory:", homePath);
|
|
|
|
const personalConfigArray = configArrayFactory.loadInDirectory(
|
|
homePath,
|
|
{ name: "PersonalConfig" }
|
|
);
|
|
|
|
if (
|
|
personalConfigArray.length > 0 &&
|
|
!directoryPath.startsWith(homePath)
|
|
) {
|
|
const lastElement =
|
|
personalConfigArray[personalConfigArray.length - 1];
|
|
|
|
emitDeprecationWarning(
|
|
lastElement.filePath,
|
|
"ESLINT_PERSONAL_CONFIG_LOAD"
|
|
);
|
|
}
|
|
|
|
finalConfigArray = finalConfigArray.concat(personalConfigArray);
|
|
}
|
|
|
|
// Apply CLI options.
|
|
if (cliConfigArray.length > 0) {
|
|
finalConfigArray = finalConfigArray.concat(cliConfigArray);
|
|
}
|
|
|
|
// Validate rule settings and environments.
|
|
const validator = new ConfigValidator({
|
|
builtInRules
|
|
});
|
|
|
|
validator.validateConfigArray(finalConfigArray);
|
|
|
|
// Cache it.
|
|
Object.freeze(finalConfigArray);
|
|
finalizeCache.set(configArray, finalConfigArray);
|
|
|
|
debug$1(
|
|
"Configuration was determined: %o on %s",
|
|
finalConfigArray,
|
|
directoryPath
|
|
);
|
|
}
|
|
|
|
// At least one element (the default ignore patterns) exists.
|
|
if (!ignoreNotFoundError && useEslintrc && finalConfigArray.length <= 1) {
|
|
throw new ConfigurationNotFoundError(directoryPath);
|
|
}
|
|
|
|
return finalConfigArray;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @fileoverview Compatibility class for flat config.
|
|
* @author Nicholas C. Zakas
|
|
*/
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Helpers
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/** @typedef {import("../../shared/types").Environment} Environment */
|
|
/** @typedef {import("../../shared/types").Processor} Processor */
|
|
|
|
const debug = debugOrig__default["default"]("eslintrc:flat-compat");
|
|
const cafactory = Symbol("cafactory");
|
|
|
|
/**
|
|
* Translates an ESLintRC-style config object into a flag-config-style config
|
|
* object.
|
|
* @param {Object} eslintrcConfig An ESLintRC-style config object.
|
|
* @param {Object} options Options to help translate the config.
|
|
* @param {string} options.resolveConfigRelativeTo To the directory to resolve
|
|
* configs from.
|
|
* @param {string} options.resolvePluginsRelativeTo The directory to resolve
|
|
* plugins from.
|
|
* @param {ReadOnlyMap<string,Environment>} options.pluginEnvironments A map of plugin environment
|
|
* names to objects.
|
|
* @param {ReadOnlyMap<string,Processor>} options.pluginProcessors A map of plugin processor
|
|
* names to objects.
|
|
* @returns {Object} A flag-config-style config object.
|
|
*/
|
|
function translateESLintRC(eslintrcConfig, {
|
|
resolveConfigRelativeTo,
|
|
resolvePluginsRelativeTo,
|
|
pluginEnvironments,
|
|
pluginProcessors
|
|
}) {
|
|
|
|
const flatConfig = {};
|
|
const configs = [];
|
|
const languageOptions = {};
|
|
const linterOptions = {};
|
|
const keysToCopy = ["settings", "rules", "processor"];
|
|
const languageOptionsKeysToCopy = ["globals", "parser", "parserOptions"];
|
|
const linterOptionsKeysToCopy = ["noInlineConfig", "reportUnusedDisableDirectives"];
|
|
|
|
// check for special settings for eslint:all and eslint:recommended:
|
|
if (eslintrcConfig.settings) {
|
|
if (eslintrcConfig.settings["eslint:all"] === true) {
|
|
return ["eslint:all"];
|
|
}
|
|
|
|
if (eslintrcConfig.settings["eslint:recommended"] === true) {
|
|
return ["eslint:recommended"];
|
|
}
|
|
}
|
|
|
|
// copy over simple translations
|
|
for (const key of keysToCopy) {
|
|
if (key in eslintrcConfig && typeof eslintrcConfig[key] !== "undefined") {
|
|
flatConfig[key] = eslintrcConfig[key];
|
|
}
|
|
}
|
|
|
|
// copy over languageOptions
|
|
for (const key of languageOptionsKeysToCopy) {
|
|
if (key in eslintrcConfig && typeof eslintrcConfig[key] !== "undefined") {
|
|
|
|
// create the languageOptions key in the flat config
|
|
flatConfig.languageOptions = languageOptions;
|
|
|
|
if (key === "parser") {
|
|
debug(`Resolving parser '${languageOptions[key]}' relative to ${resolveConfigRelativeTo}`);
|
|
|
|
if (eslintrcConfig[key].error) {
|
|
throw eslintrcConfig[key].error;
|
|
}
|
|
|
|
languageOptions[key] = eslintrcConfig[key].definition;
|
|
continue;
|
|
}
|
|
|
|
// clone any object values that are in the eslintrc config
|
|
if (eslintrcConfig[key] && typeof eslintrcConfig[key] === "object") {
|
|
languageOptions[key] = {
|
|
...eslintrcConfig[key]
|
|
};
|
|
} else {
|
|
languageOptions[key] = eslintrcConfig[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
// copy over linterOptions
|
|
for (const key of linterOptionsKeysToCopy) {
|
|
if (key in eslintrcConfig && typeof eslintrcConfig[key] !== "undefined") {
|
|
flatConfig.linterOptions = linterOptions;
|
|
linterOptions[key] = eslintrcConfig[key];
|
|
}
|
|
}
|
|
|
|
// move ecmaVersion a level up
|
|
if (languageOptions.parserOptions) {
|
|
|
|
if ("ecmaVersion" in languageOptions.parserOptions) {
|
|
languageOptions.ecmaVersion = languageOptions.parserOptions.ecmaVersion;
|
|
delete languageOptions.parserOptions.ecmaVersion;
|
|
}
|
|
|
|
if ("sourceType" in languageOptions.parserOptions) {
|
|
languageOptions.sourceType = languageOptions.parserOptions.sourceType;
|
|
delete languageOptions.parserOptions.sourceType;
|
|
}
|
|
|
|
// check to see if we even need parserOptions anymore and remove it if not
|
|
if (Object.keys(languageOptions.parserOptions).length === 0) {
|
|
delete languageOptions.parserOptions;
|
|
}
|
|
}
|
|
|
|
// overrides
|
|
if (eslintrcConfig.criteria) {
|
|
flatConfig.files = [absoluteFilePath => eslintrcConfig.criteria.test(absoluteFilePath)];
|
|
}
|
|
|
|
// translate plugins
|
|
if (eslintrcConfig.plugins && typeof eslintrcConfig.plugins === "object") {
|
|
debug(`Translating plugins: ${eslintrcConfig.plugins}`);
|
|
|
|
flatConfig.plugins = {};
|
|
|
|
for (const pluginName of Object.keys(eslintrcConfig.plugins)) {
|
|
|
|
debug(`Translating plugin: ${pluginName}`);
|
|
debug(`Resolving plugin '${pluginName} relative to ${resolvePluginsRelativeTo}`);
|
|
|
|
const { definition: plugin, error } = eslintrcConfig.plugins[pluginName];
|
|
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
|
|
flatConfig.plugins[pluginName] = plugin;
|
|
|
|
// create a config for any processors
|
|
if (plugin.processors) {
|
|
for (const processorName of Object.keys(plugin.processors)) {
|
|
if (processorName.startsWith(".")) {
|
|
debug(`Assigning processor: ${pluginName}/${processorName}`);
|
|
|
|
configs.unshift({
|
|
files: [`**/*${processorName}`],
|
|
processor: pluginProcessors.get(`${pluginName}/${processorName}`)
|
|
});
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// translate env - must come after plugins
|
|
if (eslintrcConfig.env && typeof eslintrcConfig.env === "object") {
|
|
for (const envName of Object.keys(eslintrcConfig.env)) {
|
|
|
|
// only add environments that are true
|
|
if (eslintrcConfig.env[envName]) {
|
|
debug(`Translating environment: ${envName}`);
|
|
|
|
if (environments.has(envName)) {
|
|
|
|
// built-in environments should be defined first
|
|
configs.unshift(...translateESLintRC(environments.get(envName), {
|
|
resolveConfigRelativeTo,
|
|
resolvePluginsRelativeTo
|
|
}));
|
|
} else if (pluginEnvironments.has(envName)) {
|
|
|
|
// if the environment comes from a plugin, it should come after the plugin config
|
|
configs.push(...translateESLintRC(pluginEnvironments.get(envName), {
|
|
resolveConfigRelativeTo,
|
|
resolvePluginsRelativeTo
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// only add if there are actually keys in the config
|
|
if (Object.keys(flatConfig).length > 0) {
|
|
configs.push(flatConfig);
|
|
}
|
|
|
|
return configs;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Exports
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* A compatibility class for working with configs.
|
|
*/
|
|
class FlatCompat {
|
|
|
|
constructor({
|
|
baseDirectory = process.cwd(),
|
|
resolvePluginsRelativeTo = baseDirectory
|
|
} = {}) {
|
|
this.baseDirectory = baseDirectory;
|
|
this.resolvePluginsRelativeTo = resolvePluginsRelativeTo;
|
|
this[cafactory] = new ConfigArrayFactory({
|
|
cwd: baseDirectory,
|
|
resolvePluginsRelativeTo,
|
|
getEslintAllConfig: () => ({ settings: { "eslint:all": true } }),
|
|
getEslintRecommendedConfig: () => ({ settings: { "eslint:recommended": true } })
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Translates an ESLintRC-style config into a flag-config-style config.
|
|
* @param {Object} eslintrcConfig The ESLintRC-style config object.
|
|
* @returns {Object} A flag-config-style config object.
|
|
*/
|
|
config(eslintrcConfig) {
|
|
const eslintrcArray = this[cafactory].create(eslintrcConfig, {
|
|
basePath: this.baseDirectory
|
|
});
|
|
|
|
const flatArray = [];
|
|
let hasIgnorePatterns = false;
|
|
|
|
eslintrcArray.forEach(configData => {
|
|
if (configData.type === "config") {
|
|
hasIgnorePatterns = hasIgnorePatterns || configData.ignorePattern;
|
|
flatArray.push(...translateESLintRC(configData, {
|
|
resolveConfigRelativeTo: path__default["default"].join(this.baseDirectory, "__placeholder.js"),
|
|
resolvePluginsRelativeTo: path__default["default"].join(this.resolvePluginsRelativeTo, "__placeholder.js"),
|
|
pluginEnvironments: eslintrcArray.pluginEnvironments,
|
|
pluginProcessors: eslintrcArray.pluginProcessors
|
|
}));
|
|
}
|
|
});
|
|
|
|
// combine ignorePatterns to emulate ESLintRC behavior better
|
|
if (hasIgnorePatterns) {
|
|
flatArray.unshift({
|
|
ignores: [filePath => {
|
|
|
|
// Compute the final config for this file.
|
|
// This filters config array elements by `files`/`excludedFiles` then merges the elements.
|
|
const finalConfig = eslintrcArray.extractConfig(filePath);
|
|
|
|
// Test the `ignorePattern` properties of the final config.
|
|
return Boolean(finalConfig.ignores) && finalConfig.ignores(filePath);
|
|
}]
|
|
});
|
|
}
|
|
|
|
return flatArray;
|
|
}
|
|
|
|
/**
|
|
* Translates the `env` section of an ESLintRC-style config.
|
|
* @param {Object} envConfig The `env` section of an ESLintRC config.
|
|
* @returns {Object} A flag-config object representing the environments.
|
|
*/
|
|
env(envConfig) {
|
|
return this.config({
|
|
env: envConfig
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Translates the `extends` section of an ESLintRC-style config.
|
|
* @param {...string} configsToExtend The names of the configs to load.
|
|
* @returns {Object} A flag-config object representing the config.
|
|
*/
|
|
extends(...configsToExtend) {
|
|
return this.config({
|
|
extends: configsToExtend
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Translates the `plugins` section of an ESLintRC-style config.
|
|
* @param {...string} plugins The names of the plugins to load.
|
|
* @returns {Object} A flag-config object representing the plugins.
|
|
*/
|
|
plugins(...plugins) {
|
|
return this.config({
|
|
plugins
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @fileoverview Package exports for @eslint/eslintrc
|
|
* @author Nicholas C. Zakas
|
|
*/
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Exports
|
|
//-----------------------------------------------------------------------------
|
|
|
|
const Legacy = {
|
|
ConfigArray,
|
|
createConfigArrayFactoryContext: createContext,
|
|
CascadingConfigArrayFactory,
|
|
ConfigArrayFactory,
|
|
ConfigDependency,
|
|
ExtractedConfig,
|
|
IgnorePattern,
|
|
OverrideTester,
|
|
getUsedExtractedConfigs,
|
|
environments,
|
|
|
|
// shared
|
|
ConfigOps,
|
|
ConfigValidator,
|
|
ModuleResolver,
|
|
naming
|
|
};
|
|
|
|
exports.FlatCompat = FlatCompat;
|
|
exports.Legacy = Legacy;
|
|
//# sourceMappingURL=eslintrc.cjs.map
|