init source

This commit is contained in:
Le Viet
2022-03-07 22:07:57 +07:00
parent e4376f3777
commit 8aba590a8d
11240 changed files with 1012977 additions and 0 deletions
+417
View File
@@ -0,0 +1,417 @@
/**
* @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>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const os = require("os");
const path = require("path");
const { validateConfigArray } = require("../shared/config-validator");
const { ConfigArrayFactory } = require("./config-array-factory");
const { ConfigArray, ConfigDependency } = require("./config-array");
const loadRules = require("./load-rules");
const debug = require("debug")("eslint: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 {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`, `--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[]} [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.
*/
/**
* @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[]|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.
*/
/** @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
}) {
const baseConfigArray = configArrayFactory.create(
baseConfigData,
{ name: "BaseConfig" }
);
if (rulePaths && rulePaths.length > 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.
*/
baseConfigArray.push({
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,
specificConfigPath
}) {
const cliConfigArray = configArrayFactory.create(
cliConfigData,
{ name: "CLIOptions" }
);
if (specificConfigPath) {
cliConfigArray.unshift(
...configArrayFactory.loadFile(
specificConfigPath,
{ name: "--config" }
)
);
}
return cliConfigArray;
}
/**
* The error type when there are files matched by a glob, but all of them have been ignored.
*/
class ConfigurationNotFoundError extends Error {
/**
* @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(),
resolvePluginsRelativeTo = cwd,
rulePaths = [],
specificConfigPath = null,
useEslintrc = true
} = {}) {
const configArrayFactory = new ConfigArrayFactory({
additionalPluginPool,
cwd,
resolvePluginsRelativeTo
});
internalSlotsMap.set(this, {
baseConfigArray: createBaseConfigArray({
baseConfigData,
configArrayFactory,
cwd,
rulePaths
}),
baseConfigData,
cliConfigArray: createCLIConfigArray({
cliConfigData,
configArrayFactory,
specificConfigPath
}),
cliConfigData,
configArrayFactory,
configCache: new Map(),
cwd,
finalizeCache: new WeakMap(),
rulePaths,
specificConfigPath,
useEslintrc
});
}
/**
* 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.
* @returns {ConfigArray} The config array of the file.
*/
getConfigArrayForFile(filePath) {
const {
baseConfigArray,
cliConfigArray,
cwd
} = internalSlotsMap.get(this);
if (!filePath) {
return new ConfigArray(...baseConfigArray, ...cliConfigArray);
}
const directoryPath = path.dirname(path.resolve(cwd, filePath));
debug(`Load config files for ${directoryPath}.`);
return this._finalizeConfigArray(
this._loadConfigInAncestors(directoryPath),
directoryPath
);
}
/**
* 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.
* @returns {ConfigArray} The loaded config.
* @private
*/
_loadConfigInAncestors(directoryPath) {
const {
baseConfigArray,
configArrayFactory,
configCache,
cwd,
useEslintrc
} = internalSlotsMap.get(this);
if (!useEslintrc) {
return baseConfigArray;
}
let configArray = configCache.get(directoryPath);
// Hit cache.
if (configArray) {
debug(`Cache hit: ${directoryPath}.`);
return configArray;
}
debug(`No cache found: ${directoryPath}.`);
const homePath = os.homedir();
// Consider this is root.
if (directoryPath === homePath && cwd !== homePath) {
debug("Stop traversing because of considered root.");
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("Stop traversing because of 'EACCES' error.");
return this._cacheConfig(directoryPath, baseConfigArray);
}
throw error;
}
if (configArray.length > 0 && configArray.isRoot()) {
debug("Stop traversing because of 'root:true'.");
configArray.unshift(...baseConfigArray);
return this._cacheConfig(directoryPath, configArray);
}
// Load from the ancestors and merge it.
const parentPath = path.dirname(directoryPath);
const parentConfigArray = parentPath && parentPath !== directoryPath
? this._loadConfigInAncestors(parentPath)
: 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.
* @returns {ConfigArray} The loaded config.
* @private
*/
_finalizeConfigArray(configArray, directoryPath) {
const {
cliConfigArray,
configArrayFactory,
finalizeCache,
useEslintrc
} = 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.
) {
debug("Loading the config file of the home directory.");
finalConfigArray = configArrayFactory.loadInDirectory(
os.homedir(),
{ name: "PersonalConfig", parent: finalConfigArray }
);
}
// Apply CLI options.
if (cliConfigArray.length > 0) {
finalConfigArray = finalConfigArray.concat(cliConfigArray);
}
// Validate rule settings and environments.
validateConfigArray(finalConfigArray);
// Cache it.
Object.freeze(finalConfigArray);
finalizeCache.set(configArray, finalConfigArray);
debug(
"Configuration was determined: %o on %s",
finalConfigArray,
directoryPath
);
}
if (useEslintrc && finalConfigArray.length === 0) {
throw new ConfigurationNotFoundError(directoryPath);
}
return finalConfigArray;
}
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = { CascadingConfigArrayFactory };
+996
View File
@@ -0,0 +1,996 @@
/**
* @fileoverview Main CLI object.
* @author Nicholas C. Zakas
*/
"use strict";
/*
* The CLI object should *not* call process.exit() directly. It should only return
* exit codes. This allows other programs to use the CLI object and still control
* when the program exits.
*/
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs");
const path = require("path");
const defaultOptions = require("../../conf/default-cli-options");
const pkg = require("../../package.json");
const ConfigOps = require("../shared/config-ops");
const naming = require("../shared/naming");
const ModuleResolver = require("../shared/relative-module-resolver");
const { Linter } = require("../linter");
const builtInRules = require("../rules");
const { CascadingConfigArrayFactory } = require("./cascading-config-array-factory");
const { getUsedExtractedConfigs } = require("./config-array");
const { FileEnumerator } = require("./file-enumerator");
const hash = require("./hash");
const { IgnoredPaths } = require("./ignored-paths");
const LintResultCache = require("./lint-result-cache");
const debug = require("debug")("eslint:cli-engine");
const validFixTypes = new Set(["problem", "suggestion", "layout"]);
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
// For VSCode IntelliSense
/** @typedef {import("../shared/types").ConfigData} ConfigData */
/** @typedef {import("../shared/types").LintMessage} LintMessage */
/** @typedef {import("../shared/types").ParserOptions} ParserOptions */
/** @typedef {import("../shared/types").Plugin} Plugin */
/** @typedef {import("../shared/types").RuleConf} RuleConf */
/** @typedef {import("../shared/types").Rule} Rule */
/** @typedef {ReturnType<CascadingConfigArrayFactory["getConfigArrayForFile"]>} ConfigArray */
/** @typedef {ReturnType<ConfigArray["extractConfig"]>} ExtractedConfig */
/**
* The options to configure a CLI engine with.
* @typedef {Object} CLIEngineOptions
* @property {boolean} allowInlineConfig Enable or disable inline configuration comments.
* @property {ConfigData} baseConfig Base config object, extended by all configs used with this CLIEngine instance
* @property {boolean} cache Enable result caching.
* @property {string} cacheLocation The cache file to use instead of .eslintcache.
* @property {string} configFile The configuration file to use.
* @property {string} cwd The value to use for the current working directory.
* @property {string[]} envs An array of environments to load.
* @property {string[]} extensions An array of file extensions to check.
* @property {boolean|Function} fix Execute in autofix mode. If a function, should return a boolean.
* @property {string[]} fixTypes Array of rule types to apply fixes for.
* @property {string[]} globals An array of global variables to declare.
* @property {boolean} ignore False disables use of .eslintignore.
* @property {string} ignorePath The ignore file to use instead of .eslintignore.
* @property {string} ignorePattern A glob pattern of files to ignore.
* @property {boolean} useEslintrc False disables looking for .eslintrc
* @property {string} parser The name of the parser to use.
* @property {ParserOptions} parserOptions An object of parserOption settings to use.
* @property {string[]} plugins An array of plugins to load.
* @property {Record<string,RuleConf>} rules An object of rules to use.
* @property {string[]} rulePaths An array of directories to load custom rules from.
* @property {boolean} reportUnusedDisableDirectives `true` adds reports for unused eslint-disable directives
* @property {boolean} globInputPaths Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file.
* @property {string} resolvePluginsRelativeTo The folder where plugins should be resolved from, defaulting to the CWD
*/
/**
* A linting result.
* @typedef {Object} LintResult
* @property {string} filePath The path to the file that was linted.
* @property {LintMessage[]} messages All of the messages for the result.
* @property {number} errorCount Number of errors for the result.
* @property {number} warningCount Number of warnings for the result.
* @property {number} fixableErrorCount Number of fixable errors for the result.
* @property {number} fixableWarningCount Number of fixable warnings for the result.
* @property {string} [source] The source code of the file that was linted.
* @property {string} [output] The source code of the file that was linted, with as many fixes applied as possible.
*/
/**
* Information of deprecated rules.
* @typedef {Object} DeprecatedRuleInfo
* @property {string} ruleId The rule ID.
* @property {string[]} replacedBy The rule IDs that replace this deprecated rule.
*/
/**
* Linting results.
* @typedef {Object} LintReport
* @property {LintResult[]} results All of the result.
* @property {number} errorCount Number of errors for the result.
* @property {number} warningCount Number of warnings for the result.
* @property {number} fixableErrorCount Number of fixable errors for the result.
* @property {number} fixableWarningCount Number of fixable warnings for the result.
* @property {DeprecatedRuleInfo[]} usedDeprecatedRules The list of used deprecated rules.
*/
/**
* Private data for CLIEngine.
* @typedef {Object} CLIEngineInternalSlots
* @property {Map<string, Plugin>} additionalPluginPool The map for additional plugins.
* @property {string} cacheFilePath The path to the cache of lint results.
* @property {CascadingConfigArrayFactory} configArrayFactory The factory of configs.
* @property {FileEnumerator} fileEnumerator The file enumerator.
* @property {IgnoredPaths} ignoredPaths The ignored paths.
* @property {ConfigArray[]} lastConfigArrays The list of config arrays that the last `executeOnFiles` or `executeOnText` used.
* @property {LintResultCache|null} lintResultCache The cache of lint results.
* @property {Linter} linter The linter instance which has loaded rules.
* @property {CLIEngineOptions} options The normalized options of this instance.
*/
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/** @type {WeakMap<CLIEngine, CLIEngineInternalSlots>} */
const internalSlotsMap = new WeakMap();
/**
* Determines if each fix type in an array is supported by ESLint and throws
* an error if not.
* @param {string[]} fixTypes An array of fix types to check.
* @returns {void}
* @throws {Error} If an invalid fix type is found.
*/
function validateFixTypes(fixTypes) {
for (const fixType of fixTypes) {
if (!validFixTypes.has(fixType)) {
throw new Error(`Invalid fix type "${fixType}" found.`);
}
}
}
/**
* It will calculate the error and warning count for collection of messages per file
* @param {LintMessage[]} messages - Collection of messages
* @returns {Object} Contains the stats
* @private
*/
function calculateStatsPerFile(messages) {
return messages.reduce((stat, message) => {
if (message.fatal || message.severity === 2) {
stat.errorCount++;
if (message.fix) {
stat.fixableErrorCount++;
}
} else {
stat.warningCount++;
if (message.fix) {
stat.fixableWarningCount++;
}
}
return stat;
}, {
errorCount: 0,
warningCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0
});
}
/**
* It will calculate the error and warning count for collection of results from all files
* @param {LintResult[]} results - Collection of messages from all the files
* @returns {Object} Contains the stats
* @private
*/
function calculateStatsPerRun(results) {
return results.reduce((stat, result) => {
stat.errorCount += result.errorCount;
stat.warningCount += result.warningCount;
stat.fixableErrorCount += result.fixableErrorCount;
stat.fixableWarningCount += result.fixableWarningCount;
return stat;
}, {
errorCount: 0,
warningCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0
});
}
/**
* Processes an source code using ESLint.
* @param {Object} config The config object.
* @param {string} config.text The source code to verify.
* @param {string} config.cwd The path to the current working directory.
* @param {string|undefined} config.filePath The path to the file of `text`. If this is undefined, it uses `<text>`.
* @param {ConfigArray} config.config The config.
* @param {boolean} config.fix If `true` then it does fix.
* @param {boolean} config.allowInlineConfig If `true` then it uses directive comments.
* @param {boolean} config.reportUnusedDisableDirectives If `true` then it reports unused `eslint-disable` comments.
* @param {RegExp} config.extensionRegExp The `RegExp` object that tests if a file path has the allowed file extensions.
* @param {Linter} config.linter The linter instance to verify.
* @returns {LintResult} The result of linting.
* @private
*/
function verifyText({
text,
cwd,
filePath: providedFilePath,
config,
fix,
allowInlineConfig,
reportUnusedDisableDirectives,
extensionRegExp,
linter
}) {
const filePath = providedFilePath || "<text>";
debug(`Lint ${filePath}`);
/*
* Verify.
* `config.extractConfig(filePath)` requires an absolute path, but `linter`
* doesn't know CWD, so it gives `linter` an absolute path always.
*/
const filePathToVerify = filePath === "<text>" ? path.join(cwd, filePath) : filePath;
const { fixed, messages, output } = linter.verifyAndFix(
text,
config,
{
allowInlineConfig,
filename: filePathToVerify,
fix,
reportUnusedDisableDirectives,
/**
* Check if the linter should adopt a given code block or not.
* Currently, the linter adopts code blocks if the name matches `--ext` option.
* In the future, `overrides` in the configuration would affect the adoption (https://github.com/eslint/rfcs/pull/20).
* @param {string} blockFilename The virtual filename of a code block.
* @returns {boolean} `true` if the linter should adopt the code block.
*/
filterCodeBlock(blockFilename) {
return extensionRegExp.test(blockFilename);
}
}
);
// Tweak and return.
const result = {
filePath,
messages,
...calculateStatsPerFile(messages)
};
if (fixed) {
result.output = output;
}
if (
result.errorCount + result.warningCount > 0 &&
typeof result.output === "undefined"
) {
result.source = text;
}
return result;
}
/**
* Returns result with warning by ignore settings
* @param {string} filePath - File path of checked code
* @param {string} baseDir - Absolute path of base directory
* @returns {LintResult} Result with single warning
* @private
*/
function createIgnoreResult(filePath, baseDir) {
let message;
const isHidden = /^\./u.test(path.basename(filePath));
const isInNodeModules = baseDir && path.relative(baseDir, filePath).startsWith("node_modules");
const isInBowerComponents = baseDir && path.relative(baseDir, filePath).startsWith("bower_components");
if (isHidden) {
message = "File ignored by default. Use a negated ignore pattern (like \"--ignore-pattern '!<relative/path/to/filename>'\") to override.";
} else if (isInNodeModules) {
message = "File ignored by default. Use \"--ignore-pattern '!node_modules/*'\" to override.";
} else if (isInBowerComponents) {
message = "File ignored by default. Use \"--ignore-pattern '!bower_components/*'\" to override.";
} else {
message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override.";
}
return {
filePath: path.resolve(filePath),
messages: [
{
fatal: false,
severity: 1,
message
}
],
errorCount: 0,
warningCount: 1,
fixableErrorCount: 0,
fixableWarningCount: 0
};
}
/**
* Get a rule.
* @param {string} ruleId The rule ID to get.
* @param {ConfigArray[]} configArrays The config arrays that have plugin rules.
* @returns {Rule|null} The rule or null.
*/
function getRule(ruleId, configArrays) {
for (const configArray of configArrays) {
const rule = configArray.pluginRules.get(ruleId);
if (rule) {
return rule;
}
}
return builtInRules.get(ruleId) || null;
}
/**
* Collect used deprecated rules.
* @param {ConfigArray[]} usedConfigArrays The config arrays which were used.
* @returns {IterableIterator<DeprecatedRuleInfo>} Used deprecated rules.
*/
function *iterateRuleDeprecationWarnings(usedConfigArrays) {
const processedRuleIds = new Set();
// Flatten used configs.
/** @type {ExtractedConfig[]} */
const configs = [].concat(
...usedConfigArrays.map(getUsedExtractedConfigs)
);
// Traverse rule configs.
for (const config of configs) {
for (const [ruleId, ruleConfig] of Object.entries(config.rules)) {
// Skip if it was processed.
if (processedRuleIds.has(ruleId)) {
continue;
}
processedRuleIds.add(ruleId);
// Skip if it's not used.
if (!ConfigOps.getRuleSeverity(ruleConfig)) {
continue;
}
const rule = getRule(ruleId, usedConfigArrays);
// Skip if it's not deprecated.
if (!(rule && rule.meta && rule.meta.deprecated)) {
continue;
}
// This rule was used and deprecated.
yield {
ruleId,
replacedBy: rule.meta.replacedBy || []
};
}
}
}
/**
* Checks if the given message is an error message.
* @param {LintMessage} message The message to check.
* @returns {boolean} Whether or not the message is an error message.
* @private
*/
function isErrorMessage(message) {
return message.severity === 2;
}
/**
* return the cacheFile to be used by eslint, based on whether the provided parameter is
* a directory or looks like a directory (ends in `path.sep`), in which case the file
* name will be the `cacheFile/.cache_hashOfCWD`
*
* if cacheFile points to a file or looks like a file then in will just use that file
*
* @param {string} cacheFile The name of file to be used to store the cache
* @param {string} cwd Current working directory
* @returns {string} the resolved path to the cache file
*/
function getCacheFile(cacheFile, cwd) {
/*
* make sure the path separators are normalized for the environment/os
* keeping the trailing path separator if present
*/
const normalizedCacheFile = path.normalize(cacheFile);
const resolvedCacheFile = path.resolve(cwd, normalizedCacheFile);
const looksLikeADirectory = normalizedCacheFile.slice(-1) === path.sep;
/**
* return the name for the cache file in case the provided parameter is a directory
* @returns {string} the resolved path to the cacheFile
*/
function getCacheFileForDirectory() {
return path.join(resolvedCacheFile, `.cache_${hash(cwd)}`);
}
let fileStats;
try {
fileStats = fs.lstatSync(resolvedCacheFile);
} catch (ex) {
fileStats = null;
}
/*
* in case the file exists we need to verify if the provided path
* is a directory or a file. If it is a directory we want to create a file
* inside that directory
*/
if (fileStats) {
/*
* is a directory or is a file, but the original file the user provided
* looks like a directory but `path.resolve` removed the `last path.sep`
* so we need to still treat this like a directory
*/
if (fileStats.isDirectory() || looksLikeADirectory) {
return getCacheFileForDirectory();
}
// is file so just use that file
return resolvedCacheFile;
}
/*
* here we known the file or directory doesn't exist,
* so we will try to infer if its a directory if it looks like a directory
* for the current operating system.
*/
// if the last character passed is a path separator we assume is a directory
if (looksLikeADirectory) {
return getCacheFileForDirectory();
}
return resolvedCacheFile;
}
/**
* Convert a string array to a boolean map.
* @param {string[]|null} keys The keys to assign true.
* @param {boolean} defaultValue The default value for each property.
* @param {string} displayName The property name which is used in error message.
* @returns {Record<string,boolean>} The boolean map.
*/
function toBooleanMap(keys, defaultValue, displayName) {
if (keys && !Array.isArray(keys)) {
throw new Error(`${displayName} must be an array.`);
}
if (keys && keys.length > 0) {
return keys.reduce((map, def) => {
const [key, value] = def.split(":");
if (key !== "__proto__") {
map[key] = value === void 0
? defaultValue
: value === "true";
}
return map;
}, {});
}
return void 0;
}
/**
* Create a config data from CLI options.
* @param {CLIEngineOptions} options The options
* @returns {ConfigData|null} The created config data.
*/
function createConfigDataFromOptions(options) {
const { parser, parserOptions, plugins, rules } = options;
const env = toBooleanMap(options.envs, true, "envs");
const globals = toBooleanMap(options.globals, false, "globals");
if (
env === void 0 &&
globals === void 0 &&
parser === void 0 &&
parserOptions === void 0 &&
plugins === void 0 &&
rules === void 0
) {
return null;
}
return { env, globals, parser, parserOptions, plugins, rules };
}
/**
* Checks whether a directory exists at the given location
* @param {string} resolvedPath A path from the CWD
* @returns {boolean} `true` if a directory exists
*/
function directoryExists(resolvedPath) {
try {
return fs.statSync(resolvedPath).isDirectory();
} catch (error) {
if (error && error.code === "ENOENT") {
return false;
}
throw error;
}
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
class CLIEngine {
/**
* Creates a new instance of the core CLI engine.
* @param {CLIEngineOptions} providedOptions The options for this instance.
*/
constructor(providedOptions) {
const options = Object.assign(
Object.create(null),
defaultOptions,
{ cwd: process.cwd() },
providedOptions
);
if (options.fix === void 0) {
options.fix = false;
}
const additionalPluginPool = new Map();
const cacheFilePath = getCacheFile(
options.cacheLocation || options.cacheFile,
options.cwd
);
const configArrayFactory = new CascadingConfigArrayFactory({
additionalPluginPool,
baseConfig: options.baseConfig || null,
cliConfig: createConfigDataFromOptions(options),
cwd: options.cwd,
resolvePluginsRelativeTo: options.resolvePluginsRelativeTo,
rulePaths: options.rulePaths,
specificConfigPath: options.configFile,
useEslintrc: options.useEslintrc
});
const ignoredPaths = new IgnoredPaths(options);
const fileEnumerator = new FileEnumerator({
configArrayFactory,
cwd: options.cwd,
extensions: options.extensions,
globInputPaths: options.globInputPaths,
ignore: options.ignore,
ignoredPaths
});
const lintResultCache =
options.cache ? new LintResultCache(cacheFilePath) : null;
const linter = new Linter();
/** @type {ConfigArray[]} */
const lastConfigArrays = [configArrayFactory.getConfigArrayForFile()];
// Store private data.
internalSlotsMap.set(this, {
additionalPluginPool,
cacheFilePath,
configArrayFactory,
fileEnumerator,
ignoredPaths,
lastConfigArrays,
lintResultCache,
linter,
options
});
// setup special filter for fixes
if (options.fix && options.fixTypes && options.fixTypes.length > 0) {
debug(`Using fix types ${options.fixTypes}`);
// throw an error if any invalid fix types are found
validateFixTypes(options.fixTypes);
// convert to Set for faster lookup
const fixTypes = new Set(options.fixTypes);
// save original value of options.fix in case it's a function
const originalFix = (typeof options.fix === "function")
? options.fix : () => true;
options.fix = message => {
const rule = message.ruleId && getRule(message.ruleId, lastConfigArrays);
const matches = rule && rule.meta && fixTypes.has(rule.meta.type);
return matches && originalFix(message);
};
}
}
getRules() {
const { lastConfigArrays } = internalSlotsMap.get(this);
return new Map(function *() {
yield* builtInRules;
for (const configArray of lastConfigArrays) {
yield* configArray.pluginRules;
}
}());
}
/**
* Returns results that only contains errors.
* @param {LintResult[]} results The results to filter.
* @returns {LintResult[]} The filtered results.
*/
static getErrorResults(results) {
const filtered = [];
results.forEach(result => {
const filteredMessages = result.messages.filter(isErrorMessage);
if (filteredMessages.length > 0) {
filtered.push({
...result,
messages: filteredMessages,
errorCount: filteredMessages.length,
warningCount: 0,
fixableErrorCount: result.fixableErrorCount,
fixableWarningCount: 0
});
}
});
return filtered;
}
/**
* Outputs fixes from the given results to files.
* @param {LintReport} report The report object created by CLIEngine.
* @returns {void}
*/
static outputFixes(report) {
report.results.filter(result => Object.prototype.hasOwnProperty.call(result, "output")).forEach(result => {
fs.writeFileSync(result.filePath, result.output);
});
}
/**
* Add a plugin by passing its configuration
* @param {string} name Name of the plugin.
* @param {Plugin} pluginObject Plugin configuration object.
* @returns {void}
*/
addPlugin(name, pluginObject) {
const {
additionalPluginPool,
configArrayFactory
} = internalSlotsMap.get(this);
additionalPluginPool.set(name, pluginObject);
configArrayFactory.clearCache();
}
/**
* Resolves the patterns passed into executeOnFiles() into glob-based patterns
* for easier handling.
* @param {string[]} patterns The file patterns passed on the command line.
* @returns {string[]} The equivalent glob patterns.
*/
resolveFileGlobPatterns(patterns) {
const { options } = internalSlotsMap.get(this);
if (options.globInputPaths === false) {
return patterns.filter(Boolean);
}
const extensions = options.extensions.map(ext => ext.replace(/^\./u, ""));
const dirSuffix = `/**/*.{${extensions.join(",")}}`;
return patterns.filter(Boolean).map(pathname => {
const resolvedPath = path.resolve(options.cwd, pathname);
const newPath = directoryExists(resolvedPath)
? pathname.replace(/[/\\]$/u, "") + dirSuffix
: pathname;
return path.normalize(newPath).replace(/\\/gu, "/");
});
}
/**
* Executes the current configuration on an array of file and directory names.
* @param {string[]} patterns An array of file and directory names.
* @returns {LintReport} The results for all files that were linted.
*/
executeOnFiles(patterns) {
const {
cacheFilePath,
fileEnumerator,
lastConfigArrays,
lintResultCache,
linter,
options: {
allowInlineConfig,
cache,
cwd,
fix,
reportUnusedDisableDirectives
}
} = internalSlotsMap.get(this);
const results = [];
const startTime = Date.now();
// Clear the last used config arrays.
lastConfigArrays.length = 0;
// Delete cache file; should this do here?
if (!cache) {
try {
fs.unlinkSync(cacheFilePath);
} catch (error) {
const errorCode = error && error.code;
// Ignore errors when no such file exists or file system is read only (and cache file does not exist)
if (errorCode !== "ENOENT" && !(errorCode === "EROFS" && !fs.existsSync(cacheFilePath))) {
throw error;
}
}
}
// Iterate source code files.
for (const { config, filePath, ignored } of fileEnumerator.iterateFiles(patterns)) {
if (ignored) {
results.push(createIgnoreResult(filePath, cwd));
continue;
}
/*
* Store used configs for:
* - this method uses to collect used deprecated rules.
* - `getRules()` method uses to collect all loaded rules.
* - `--fix-type` option uses to get the loaded rule's meta data.
*/
if (!lastConfigArrays.includes(config)) {
lastConfigArrays.push(config);
}
// Skip if there is cached result.
if (lintResultCache) {
const cachedResult =
lintResultCache.getCachedLintResults(filePath, config);
if (cachedResult) {
const hadMessages =
cachedResult.messages &&
cachedResult.messages.length > 0;
if (hadMessages && fix) {
debug(`Reprocessing cached file to allow autofix: ${filePath}`);
} else {
debug(`Skipping file since it hasn't changed: ${filePath}`);
results.push(cachedResult);
continue;
}
}
}
// Do lint.
const result = verifyText({
text: fs.readFileSync(filePath, "utf8"),
filePath,
config,
cwd,
fix,
allowInlineConfig,
reportUnusedDisableDirectives,
extensionRegExp: fileEnumerator.extensionRegExp,
linter
});
results.push(result);
/*
* Store the lint result in the LintResultCache.
* NOTE: The LintResultCache will remove the file source and any
* other properties that are difficult to serialize, and will
* hydrate those properties back in on future lint runs.
*/
if (lintResultCache) {
lintResultCache.setCachedLintResults(filePath, config, result);
}
}
// Persist the cache to disk.
if (lintResultCache) {
lintResultCache.reconcile();
}
// Collect used deprecated rules.
const usedDeprecatedRules = Array.from(
iterateRuleDeprecationWarnings(lastConfigArrays)
);
debug(`Linting complete in: ${Date.now() - startTime}ms`);
return {
results,
...calculateStatsPerRun(results),
usedDeprecatedRules
};
}
/**
* Executes the current configuration on text.
* @param {string} text A string of JavaScript code to lint.
* @param {string} [filename] An optional string representing the texts filename.
* @param {boolean} [warnIgnored] Always warn when a file is ignored
* @returns {LintReport} The results for the linting.
*/
executeOnText(text, filename, warnIgnored) {
const {
configArrayFactory,
fileEnumerator,
ignoredPaths,
lastConfigArrays,
linter,
options: {
allowInlineConfig,
cwd,
fix,
reportUnusedDisableDirectives
}
} = internalSlotsMap.get(this);
const results = [];
const startTime = Date.now();
const resolvedFilename = filename && path.resolve(cwd, filename);
// Clear the last used config arrays.
lastConfigArrays.length = 0;
if (resolvedFilename && ignoredPaths.contains(resolvedFilename)) {
if (warnIgnored) {
results.push(createIgnoreResult(resolvedFilename, cwd));
}
} else {
const config = configArrayFactory.getConfigArrayForFile(
resolvedFilename || "__placeholder__.js"
);
/*
* Store used configs for:
* - this method uses to collect used deprecated rules.
* - `getRules()` method uses to collect all loaded rules.
* - `--fix-type` option uses to get the loaded rule's meta data.
*/
lastConfigArrays.push(config);
// Do lint.
results.push(verifyText({
text,
filePath: resolvedFilename,
config,
cwd,
fix,
allowInlineConfig,
reportUnusedDisableDirectives,
extensionRegExp: fileEnumerator.extensionRegExp,
linter
}));
}
// Collect used deprecated rules.
const usedDeprecatedRules = Array.from(
iterateRuleDeprecationWarnings(lastConfigArrays)
);
debug(`Linting complete in: ${Date.now() - startTime}ms`);
return {
results,
...calculateStatsPerRun(results),
usedDeprecatedRules
};
}
/**
* Returns a configuration object for the given file based on the CLI options.
* This is the same logic used by the ESLint CLI executable to determine
* configuration for each file it processes.
* @param {string} filePath The path of the file to retrieve a config object for.
* @returns {ConfigData} A configuration object for the file.
*/
getConfigForFile(filePath) {
const { configArrayFactory, options } = internalSlotsMap.get(this);
const absolutePath = path.resolve(options.cwd, filePath);
if (directoryExists(absolutePath)) {
throw Object.assign(
new Error("'filePath' should not be a directory path."),
{ messageTemplate: "print-config-with-directory-path" }
);
}
return configArrayFactory
.getConfigArrayForFile(absolutePath)
.extractConfig(absolutePath)
.toCompatibleObjectAsConfigFileContent();
}
/**
* Checks if a given path is ignored by ESLint.
* @param {string} filePath The path of the file to check.
* @returns {boolean} Whether or not the given path is ignored.
*/
isPathIgnored(filePath) {
const { ignoredPaths } = internalSlotsMap.get(this);
return ignoredPaths.contains(filePath);
}
/**
* Returns the formatter representing the given format or null if no formatter
* with the given name can be found.
* @param {string} [format] The name of the format to load or the path to a
* custom formatter.
* @returns {Function} The formatter function or null if not found.
*/
getFormatter(format) {
// default is stylish
const resolvedFormatName = format || "stylish";
// only strings are valid formatters
if (typeof resolvedFormatName === "string") {
// replace \ with / for Windows compatibility
const normalizedFormatName = resolvedFormatName.replace(/\\/gu, "/");
const slots = internalSlotsMap.get(this);
const cwd = slots ? slots.options.cwd : process.cwd();
const namespace = naming.getNamespaceFromTerm(normalizedFormatName);
let formatterPath;
// if there's a slash, then it's a file (TODO: this check seems dubious for scoped npm packages)
if (!namespace && normalizedFormatName.indexOf("/") > -1) {
formatterPath = path.resolve(cwd, normalizedFormatName);
} else {
try {
const npmFormat = naming.normalizePackageName(normalizedFormatName, "eslint-formatter");
formatterPath = ModuleResolver.resolve(npmFormat, path.join(cwd, "__placeholder__.js"));
} catch (e) {
formatterPath = path.resolve(__dirname, "formatters", normalizedFormatName);
}
}
try {
return require(formatterPath);
} catch (ex) {
ex.message = `There was a problem loading formatter: ${formatterPath}\nError: ${ex.message}`;
throw ex;
}
} else {
return null;
}
}
}
CLIEngine.version = pkg.version;
CLIEngine.getFormatter = CLIEngine.prototype.getFormatter;
module.exports = {
CLIEngine,
/**
* Get the internal slots of a given CLIEngine instance for tests.
* @param {CLIEngine} instance The CLIEngine instance to get.
* @returns {CLIEngineInternalSlots} The internal slots.
*/
getCLIEngineInternalSlots(instance) {
return internalSlotsMap.get(instance);
}
};
+919
View File
@@ -0,0 +1,919 @@
/**
* @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`.
*
* `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>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs");
const path = require("path");
const importFresh = require("import-fresh");
const stripComments = require("strip-json-comments");
const { validateConfigSchema } = require("../shared/config-validator");
const naming = require("../shared/naming");
const ModuleResolver = require("../shared/relative-module-resolver");
const { ConfigArray, ConfigDependency, OverrideTester } = require("./config-array");
const debug = require("debug")("eslint:config-array-factory");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const eslintRecommendedPath = path.resolve(__dirname, "../../conf/eslint-recommended.js");
const eslintAllPath = path.resolve(__dirname, "../../conf/eslint-all.js");
const configFilenames = [
".eslintrc.js",
".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("./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`.
*/
/**
* @typedef {Object} ConfigArrayFactoryInternalSlots
* @property {Map<string,Plugin>} additionalPluginPool The map for additional plugins.
* @property {string} cwd The path to the current working directory.
* @property {string} resolvePluginsRelativeTo An absolute path the the directory that plugins should be resolved from.
*/
/** @type {WeakMap<ConfigArrayFactory, ConfigArrayFactoryInternalSlots>} */
const internalSlotsMap = 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.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.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(`Loading YAML config file: ${filePath}`);
// lazy load YAML to improve performance when not used
const yaml = require("js-yaml");
try {
// empty YAML file can be null, so always use
return yaml.safeLoad(readFile(filePath)) || {};
} catch (e) {
debug(`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(`Loading JSON config file: ${filePath}`);
try {
return JSON.parse(stripComments(readFile(filePath)));
} catch (e) {
debug(`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(`Loading legacy config file: ${filePath}`);
// lazy load YAML to improve performance when not used
const yaml = require("js-yaml");
try {
return yaml.safeLoad(stripComments(readFile(filePath))) || /* istanbul ignore next */ {};
} catch (e) {
debug("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(`Loading JS config file: ${filePath}`);
try {
return importFresh(filePath);
} catch (e) {
debug(`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(`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(`Error reading package.json file: ${filePath}`);
e.message = `Cannot read config 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
* @returns {Error} The error object to throw
* @private
*/
function configMissingError(configName, importerName) {
return Object.assign(
new Error(`Failed to load config "${configName}" to extend from.`),
{
messageTemplate: "extend-config-missing",
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.extname(filePath)) {
case ".js":
return loadJSConfigFile(filePath);
case ".json":
if (path.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.enabled) {
let nameAndVersion = null;
try {
const packageJsonPath = ModuleResolver.resolve(
`${request}/package.json`,
relativeTo
);
const { version = "unknown" } = require(packageJsonPath);
nameAndVersion = `${request}@${version}`;
} catch (error) {
debug("package.json was not found:", error.message);
nameAndVersion = request;
}
debug("Loaded: %s (%s)", nameAndVersion, filePath);
}
}
/**
* Concatenate two config data.
* @param {IterableIterator<ConfigArrayElement>|null} elements The config elements.
* @param {ConfigArray|null} parentConfigArray The parent config array.
* @returns {ConfigArray} The concatenated config array.
*/
function createConfigArray(elements, parentConfigArray) {
if (!elements) {
return parentConfigArray || new ConfigArray();
}
const configArray = new ConfigArray(...elements);
if (parentConfigArray && !configArray.isRoot()) {
configArray.unshift(...parentConfigArray);
}
return configArray;
}
/**
* 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 = cwd
} = {}) {
internalSlotsMap.set(this, { additionalPluginPool, cwd, resolvePluginsRelativeTo: path.resolve(cwd, resolvePluginsRelativeTo) });
}
/**
* Create `ConfigArray` instance from a config data.
* @param {ConfigData|null} configData The config data to create.
* @param {Object} [options] The options.
* @param {string} [options.filePath] The path to this config data.
* @param {string} [options.name] The config name.
* @param {ConfigArray} [options.parent] The parent config array.
* @returns {ConfigArray} Loaded config.
*/
create(configData, { filePath, name, parent } = {}) {
return createConfigArray(
configData
? this._normalizeConfigData(configData, filePath, name)
: null,
parent
);
}
/**
* Load a config file.
* @param {string} filePath The path to a config file.
* @param {Object} [options] The options.
* @param {string} [options.name] The config name.
* @param {ConfigArray} [options.parent] The parent config array.
* @returns {ConfigArray} Loaded config.
*/
loadFile(filePath, { name, parent } = {}) {
const { cwd } = internalSlotsMap.get(this);
const absolutePath = path.resolve(cwd, filePath);
return createConfigArray(
this._loadConfigData(absolutePath, name),
parent
);
}
/**
* 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.name] The config name.
* @param {ConfigArray} [options.parent] The parent config array.
* @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist.
*/
loadInDirectory(directoryPath, { name, parent } = {}) {
const { cwd } = internalSlotsMap.get(this);
const absolutePath = path.resolve(cwd, directoryPath);
return createConfigArray(
this._loadConfigDataInDirectory(absolutePath, name),
parent
);
}
/**
* Load a given config file.
* @param {string} filePath The path to a config file.
* @param {string} name The config name.
* @returns {IterableIterator<ConfigArrayElement>} Loaded config.
* @private
*/
_loadConfigData(filePath, name) {
return this._normalizeConfigData(
loadConfigFile(filePath),
filePath,
name
);
}
/**
* Load the config file in a given directory if exists.
* @param {string} directoryPath The path to a directory.
* @param {string} name The config name.
* @returns {IterableIterator<ConfigArrayElement> | null} Loaded config. `null` if any config doesn't exist.
* @private
*/
_loadConfigDataInDirectory(directoryPath, name) {
for (const filename of configFilenames) {
const filePath = path.join(directoryPath, filename);
if (fs.existsSync(filePath)) {
let configData;
try {
configData = loadConfigFile(filePath);
} catch (error) {
if (!error || error.code !== "ESLINT_CONFIG_FIELD_NOT_FOUND") {
throw error;
}
}
if (configData) {
debug(`Config file found: ${filePath}`);
return this._normalizeConfigData(configData, filePath, name);
}
}
}
debug(`Config file not found on ${directoryPath}`);
return null;
}
/**
* Normalize a given config to an array.
* @param {ConfigData} configData The config data to normalize.
* @param {string|undefined} providedFilePath The file path of this config.
* @param {string|undefined} providedName The name of this config.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
_normalizeConfigData(configData, providedFilePath, providedName) {
const { cwd } = internalSlotsMap.get(this);
const filePath = providedFilePath
? path.resolve(cwd, providedFilePath)
: "";
const name = providedName || (filePath && path.relative(cwd, filePath));
validateConfigSchema(configData, name || filePath);
return this._normalizeObjectConfigData(configData, filePath, name);
}
/**
* Normalize a given config to an array.
* @param {ConfigData|OverrideConfigData} configData The config data to normalize.
* @param {string} filePath The file path of this config.
* @param {string} name The name of this config.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
*_normalizeObjectConfigData(configData, filePath, name) {
const { cwd } = internalSlotsMap.get(this);
const { files, excludedFiles, ...configBody } = configData;
const basePath = filePath ? path.dirname(filePath) : cwd;
const criteria = OverrideTester.create(files, excludedFiles, basePath);
const elements =
this._normalizeObjectConfigDataBody(configBody, filePath, name);
// Apply the criteria to every element.
for (const element of elements) {
// Adopt the base path of the entry file (the outermost base path).
if (element.criteria) {
element.criteria.basePath = basePath;
}
/*
* Merge the criteria; this is for only file extension processors in
* `overrides` section for now.
*/
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 {string} filePath The file path of this config.
* @param {string} name The name of this config.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
*_normalizeObjectConfigDataBody(
{
env,
extends: extend,
globals,
noInlineConfig,
parser: parserName,
parserOptions,
plugins: pluginList,
processor,
reportUnusedDisableDirectives,
root,
rules,
settings,
overrides: overrideList = []
},
filePath,
name
) {
const extendList = Array.isArray(extend) ? extend : [extend];
// Flatten `extends`.
for (const extendName of extendList.filter(Boolean)) {
yield* this._loadExtends(extendName, filePath, name);
}
// Load parser & plugins.
const parser =
parserName && this._loadParser(parserName, filePath, name);
const plugins =
pluginList && this._loadPlugins(pluginList, filePath, name);
// Yield pseudo config data for file extension processors.
if (plugins) {
yield* this._takeFileExtensionProcessors(plugins, filePath, name);
}
// Yield the config data except `extends` and `overrides`.
yield {
// Debug information.
name,
filePath,
// Config data.
criteria: null,
env,
globals,
noInlineConfig,
parser,
parserOptions,
plugins,
processor,
reportUnusedDisableDirectives,
root,
rules,
settings
};
// Flatten `overries`.
for (let i = 0; i < overrideList.length; ++i) {
yield* this._normalizeObjectConfigData(
overrideList[i],
filePath,
`${name}#overrides[${i}]`
);
}
}
/**
* Load configs of an element in `extends`.
* @param {string} extendName The name of a base config.
* @param {string} importerPath The file path which has the `extends` property.
* @param {string} importerName The name of the config which has the `extends` property.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
_loadExtends(extendName, importerPath, importerName) {
debug("Loading {extends:%j} relative to %s", extendName, importerPath);
try {
if (extendName.startsWith("eslint:")) {
return this._loadExtendedBuiltInConfig(
extendName,
importerName
);
}
if (extendName.startsWith("plugin:")) {
return this._loadExtendedPluginConfig(
extendName,
importerPath,
importerName
);
}
return this._loadExtendedShareableConfig(
extendName,
importerPath,
importerName
);
} catch (error) {
error.message += `\nReferenced from: ${importerPath || importerName}`;
throw error;
}
}
/**
* Load configs of an element in `extends`.
* @param {string} extendName The name of a base config.
* @param {string} importerName The name of the config which has the `extends` property.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
_loadExtendedBuiltInConfig(extendName, importerName) {
const name = `${importerName} » ${extendName}`;
if (extendName === "eslint:recommended") {
return this._loadConfigData(eslintRecommendedPath, name);
}
if (extendName === "eslint:all") {
return this._loadConfigData(eslintAllPath, name);
}
throw configMissingError(extendName, importerName);
}
/**
* Load configs of an element in `extends`.
* @param {string} extendName The name of a base config.
* @param {string} importerPath The file path which has the `extends` property.
* @param {string} importerName The name of the config which has the `extends` property.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
_loadExtendedPluginConfig(extendName, importerPath, importerName) {
const slashIndex = extendName.lastIndexOf("/");
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, importerPath, importerName);
const configData =
plugin.definition &&
plugin.definition.configs[configName];
if (configData) {
return this._normalizeConfigData(
configData,
plugin.filePath,
`${importerName} » plugin:${plugin.id}/${configName}`
);
}
throw plugin.error || configMissingError(extendName, importerPath);
}
/**
* Load configs of an element in `extends`.
* @param {string} extendName The name of a base config.
* @param {string} importerPath The file path which has the `extends` property.
* @param {string} importerName The name of the config which has the `extends` property.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
_loadExtendedShareableConfig(extendName, importerPath, importerName) {
const { cwd } = internalSlotsMap.get(this);
const relativeTo = importerPath || path.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 = naming.normalizePackageName(
extendName,
"eslint-config"
);
}
let filePath;
try {
filePath = ModuleResolver.resolve(request, relativeTo);
} catch (error) {
/* istanbul ignore else */
if (error && error.code === "MODULE_NOT_FOUND") {
throw configMissingError(extendName, importerPath);
}
throw error;
}
writeDebugLogForLoading(request, relativeTo, filePath);
return this._loadConfigData(filePath, `${importerName} » ${request}`);
}
/**
* Load given plugins.
* @param {string[]} names The plugin names to load.
* @param {string} importerPath The path to a config file that imports it. This is just a debug info.
* @param {string} importerName The name of a config file that imports it. This is just a debug info.
* @returns {Record<string,DependentPlugin>} The loaded parser.
* @private
*/
_loadPlugins(names, importerPath, importerName) {
return names.reduce((map, name) => {
if (isFilePath(name)) {
throw new Error("Plugins array cannot includes file paths.");
}
const plugin = this._loadPlugin(name, importerPath, importerName);
map[plugin.id] = plugin;
return map;
}, {});
}
/**
* Load a given parser.
* @param {string} nameOrPath The package name or the path to a parser file.
* @param {string} importerPath The path to a config file that imports it.
* @param {string} importerName The name of a config file that imports it. This is just a debug info.
* @returns {DependentParser} The loaded parser.
*/
_loadParser(nameOrPath, importerPath, importerName) {
debug("Loading parser %j from %s", nameOrPath, importerPath);
const { cwd } = internalSlotsMap.get(this);
const relativeTo = importerPath || path.join(cwd, "__placeholder__.js");
try {
const filePath = ModuleResolver.resolve(nameOrPath, relativeTo);
writeDebugLogForLoading(nameOrPath, relativeTo, filePath);
return new ConfigDependency({
definition: require(filePath),
filePath,
id: nameOrPath,
importerName,
importerPath
});
} catch (error) {
// If the parser name is "espree", load the espree of ESLint.
if (nameOrPath === "espree") {
debug("Fallback espree.");
return new ConfigDependency({
definition: require("espree"),
filePath: require.resolve("espree"),
id: nameOrPath,
importerName,
importerPath
});
}
debug("Failed to load parser '%s' declared in '%s'.", nameOrPath, importerName);
error.message = `Failed to load parser '${nameOrPath}' declared in '${importerName}': ${error.message}`;
return new ConfigDependency({
error,
id: nameOrPath,
importerName,
importerPath
});
}
}
/**
* Load a given plugin.
* @param {string} name The plugin name to load.
* @param {string} importerPath The path to a config file that imports it. This is just a debug info.
* @param {string} importerName The name of a config file that imports it. This is just a debug info.
* @returns {DependentPlugin} The loaded plugin.
* @private
*/
_loadPlugin(name, importerPath, importerName) {
debug("Loading plugin %j from %s", name, importerPath);
const { additionalPluginPool, resolvePluginsRelativeTo } = internalSlotsMap.get(this);
const request = naming.normalizePackageName(name, "eslint-plugin");
const id = naming.getShorthandName(request, "eslint-plugin");
const relativeTo = path.join(resolvePluginsRelativeTo, "__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,
importerPath
});
}
// Check for additional pool.
const plugin =
additionalPluginPool.get(request) ||
additionalPluginPool.get(id);
if (plugin) {
return new ConfigDependency({
definition: normalizePlugin(plugin),
filePath: importerPath,
id,
importerName,
importerPath
});
}
let filePath;
let error;
try {
filePath = ModuleResolver.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,
importerName
};
}
}
if (filePath) {
try {
writeDebugLogForLoading(request, relativeTo, filePath);
return new ConfigDependency({
definition: normalizePlugin(require(filePath)),
filePath,
id,
importerName,
importerPath
});
} catch (loadError) {
error = loadError;
}
}
debug("Failed to load plugin '%s' declared in '%s'.", name, importerName);
error.message = `Failed to load plugin '${name}' declared in '${importerName}': ${error.message}`;
return new ConfigDependency({
error,
id,
importerName,
importerPath
});
}
/**
* Take file expression processors as config array elements.
* @param {Record<string,DependentPlugin>} plugins The plugin definitions.
* @param {string} filePath The file path of this config.
* @param {string} name The name of this config.
* @returns {IterableIterator<ConfigArrayElement>} The config array elements of file expression processors.
* @private
*/
*_takeFileExtensionProcessors(plugins, filePath, name) {
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}`
},
filePath,
`${name}#processors["${pluginId}/${processorId}"]`
);
}
}
}
}
}
module.exports = { ConfigArrayFactory };
+467
View File
@@ -0,0 +1,467 @@
/**
* @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 provies 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>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const { ExtractedConfig } = require("./extracted-config");
//------------------------------------------------------------------------------
// 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 {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.
*/
/**
* @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 = 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 || 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];
}
}
}
}
/**
* 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;
}
}
}
/**
* 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();
// 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;
}
// 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);
}
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.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.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);
}
}
const exportObject = {
ConfigArray,
/**
* 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
*/
getUsedExtractedConfigs(instance) {
const { cache } = internalSlotsMap.get(instance);
return Array.from(cache.values());
}
};
module.exports = exportObject;
+114
View File
@@ -0,0 +1,114 @@
/**
* @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>
*/
"use strict";
const util = require("util");
/**
* 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;
}
/**
* @returns {Object} a JSON compatible object.
*/
toJSON() {
const obj = this[util.inspect.custom]();
// Display `error.message` (`Error#message` is unenumerable).
if (obj.error instanceof Error) {
obj.error = { ...obj.error, message: obj.error.message };
}
return obj;
}
/**
* @returns {Object} an object to display by `console.log()`.
*/
[util.inspect.custom]() {
const {
definition: _ignore, // eslint-disable-line no-unused-vars
...obj
} = this;
return obj;
}
}
/** @typedef {ConfigDependency<import("../../shared/types").Parser>} DependentParser */
/** @typedef {ConfigDependency<import("../../shared/types").Plugin>} DependentPlugin */
module.exports = { ConfigDependency };
+119
View File
@@ -0,0 +1,119 @@
/**
* @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>
*/
"use strict";
// 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 */
/**
* 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 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 */
...config
} = this;
config.parser = config.parser && config.parser.filePath;
config.plugins = Object.keys(config.plugins).filter(Boolean).reverse();
return config;
}
}
module.exports = { ExtractedConfig };
+18
View File
@@ -0,0 +1,18 @@
/**
* @fileoverview `ConfigArray` class.
* @author Toru Nagashima <https://github.com/mysticatea>
*/
"use strict";
const { ConfigArray, getUsedExtractedConfigs } = require("./config-array");
const { ConfigDependency } = require("./config-dependency");
const { ExtractedConfig } = require("./extracted-config");
const { OverrideTester } = require("./override-tester");
module.exports = {
ConfigArray,
ConfigDependency,
ExtractedConfig,
OverrideTester,
getUsedExtractedConfigs
};
+193
View File
@@ -0,0 +1,193 @@
/**
* @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>
*/
"use strict";
const assert = require("assert");
const path = require("path");
const util = require("util");
const { Minimatch } = require("minimatch");
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);
const allPatterns = includePatterns.concat(excludePatterns);
if (allPatterns.length === 0) {
return null;
}
// Rejects absolute paths or relative paths to parents.
for (const pattern of allPatterns) {
if (path.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);
}
/**
* 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);
}
if (!a) {
return new OverrideTester(b.patterns, b.basePath);
}
assert.strictEqual(a.basePath, b.basePath);
return new OverrideTester(a.patterns.concat(b.patterns), a.basePath);
}
/**
* Initialize this instance.
* @param {Pattern[]} patterns The matchers.
* @param {string} basePath The base path.
*/
constructor(patterns, basePath) {
/** @type {Pattern[]} */
this.patterns = patterns;
/** @type {string} */
this.basePath = basePath;
}
/**
* 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.isAbsolute(filePath)) {
throw new Error(`'filePath' should be an absolute path, but got ${filePath}.`);
}
const relativePath = path.relative(this.basePath, filePath);
return this.patterns.every(({ includes, excludes }) => (
(!includes || includes.some(m => m.match(relativePath))) &&
(!excludes || !excludes.some(m => m.match(relativePath)))
));
}
/**
* @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
};
}
/**
* @returns {Object} an object to display by `console.log()`.
*/
[util.inspect.custom]() {
return this.toJSON();
}
}
module.exports = { OverrideTester };
+457
View File
@@ -0,0 +1,457 @@
/**
* @fileoverview `FileEnumerator` class.
*
* `FileEnumerator` class has two responsibilities:
*
* 1. Find target files by processing glob patterns.
* 2. Tie each target file and appropriate configuration.
*
* It provies a method:
*
* - `iterateFiles(patterns)`
* Iterate files which are matched by given patterns together with the
* corresponded configuration. This is for `CLIEngine#executeOnFiles()`.
* While iterating files, it loads the configuration file of each directory
* before iterate files on the directory, so we can use the configuration
* files to determine target files.
*
* @example
* const enumerator = new FileEnumerator();
* const linter = new Linter();
*
* for (const { config, filePath } of enumerator.iterateFiles(["*.js"])) {
* const code = fs.readFileSync(filePath, "utf8");
* const messages = linter.verify(code, config, filePath);
*
* console.log(messages);
* }
*
* @author Toru Nagashima <https://github.com/mysticatea>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs");
const path = require("path");
const getGlobParent = require("glob-parent");
const isGlob = require("is-glob");
const { escapeRegExp } = require("lodash");
const { Minimatch } = require("minimatch");
const { CascadingConfigArrayFactory } = require("./cascading-config-array-factory");
const { IgnoredPaths } = require("./ignored-paths");
const debug = require("debug")("eslint:file-enumerator");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const minimatchOpts = { dot: true, matchBase: true };
const dotfilesPattern = /(?:(?:^\.)|(?:[/\\]\.))[^/\\.].*/u;
const NONE = 0;
const IGNORED_SILENTLY = 1;
const IGNORED = 2;
// For VSCode intellisense
/** @typedef {ReturnType<CascadingConfigArrayFactory["getConfigArrayForFile"]>} ConfigArray */
/**
* @typedef {Object} FileEnumeratorOptions
* @property {CascadingConfigArrayFactory} [configArrayFactory] The factory for config arrays.
* @property {string} [cwd] The base directory to start lookup.
* @property {string[]} [extensions] The extensions to match files for directory patterns.
* @property {boolean} [globInputPaths] Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file.
* @property {boolean} [ignore] The flag to check ignored files.
* @property {IgnoredPaths} [ignoredPaths] The ignored paths.
* @property {string[]} [rulePaths] The value of `--rulesdir` option.
*/
/**
* @typedef {Object} FileAndConfig
* @property {string} filePath The path to a target file.
* @property {ConfigArray} config The config entries of that file.
* @property {boolean} ignored If `true` then this file should be ignored and warned because it was directly specified.
*/
/**
* @typedef {Object} FileEntry
* @property {string} filePath The path to a target file.
* @property {ConfigArray} config The config entries of that file.
* @property {NONE|IGNORED_SILENTLY|IGNORED} flag The flag.
* - `NONE` means the file is a target file.
* - `IGNORED_SILENTLY` means the file should be ignored silently.
* - `IGNORED` means the file should be ignored and warned because it was directly specified.
*/
/**
* @typedef {Object} FileEnumeratorInternalSlots
* @property {CascadingConfigArrayFactory} configArrayFactory The factory for config arrays.
* @property {string} cwd The base directory to start lookup.
* @property {RegExp} extensionRegExp The RegExp to test if a string ends with specific file extensions.
* @property {boolean} globInputPaths Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file.
* @property {boolean} ignoreFlag The flag to check ignored files.
* @property {IgnoredPaths} ignoredPathsWithDotfiles The ignored paths but don't include dot files.
* @property {IgnoredPaths} ignoredPaths The ignored paths.
*/
/** @type {WeakMap<FileEnumerator, FileEnumeratorInternalSlots>} */
const internalSlotsMap = new WeakMap();
/**
* Check if a string is a glob pattern or not.
* @param {string} pattern A glob pattern.
* @returns {boolean} `true` if the string is a glob pattern.
*/
function isGlobPattern(pattern) {
return isGlob(path.sep === "\\" ? pattern.replace(/\\/gu, "/") : pattern);
}
/**
* Get stats of a given path.
* @param {string} filePath The path to target file.
* @returns {fs.Stats|null} The stats.
* @private
*/
function statSafeSync(filePath) {
try {
return fs.statSync(filePath);
} catch (error) {
/* istanbul ignore next */
if (error.code !== "ENOENT") {
throw error;
}
return null;
}
}
/**
* Get filenames in a given path to a directory.
* @param {string} directoryPath The path to target directory.
* @returns {string[]} The filenames.
* @private
*/
function readdirSafeSync(directoryPath) {
try {
return fs.readdirSync(directoryPath);
} catch (error) {
/* istanbul ignore next */
if (error.code !== "ENOENT") {
throw error;
}
return [];
}
}
/**
* The error type when no files match a glob.
*/
class NoFilesFoundError extends Error {
/**
* @param {string} pattern - The glob pattern which was not found.
* @param {boolean} globDisabled - If `true` then the pattern was a glob pattern, but glob was disabled.
*/
constructor(pattern, globDisabled) {
super(`No files matching '${pattern}' were found${globDisabled ? " (glob was disabled)" : ""}.`);
this.messageTemplate = "file-not-found";
this.messageData = { pattern, globDisabled };
}
}
/**
* The error type when there are files matched by a glob, but all of them have been ignored.
*/
class AllFilesIgnoredError extends Error {
/**
* @param {string} pattern - The glob pattern which was not found.
*/
constructor(pattern) {
super(`All files matched by '${pattern}' are ignored.`);
this.messageTemplate = "all-files-ignored";
this.messageData = { pattern };
}
}
/**
* This class provides the functionality that enumerates every file which is
* matched by given glob patterns and that configuration.
*/
class FileEnumerator {
/**
* Initialize this enumerator.
* @param {FileEnumeratorOptions} options The options.
*/
constructor({
cwd = process.cwd(),
configArrayFactory = new CascadingConfigArrayFactory({ cwd }),
extensions = [".js"],
globInputPaths = true,
ignore = true,
ignoredPaths = new IgnoredPaths({ cwd, ignore })
} = {}) {
internalSlotsMap.set(this, {
configArrayFactory,
cwd,
extensionRegExp: new RegExp(
`.\\.(?:${extensions
.map(ext => escapeRegExp(
ext.startsWith(".")
? ext.slice(1)
: ext
))
.join("|")
})$`,
"u"
),
globInputPaths,
ignoreFlag: ignore,
ignoredPaths,
ignoredPathsWithDotfiles: new IgnoredPaths({
...ignoredPaths.options,
dotfiles: true
})
});
}
/**
* The `RegExp` object that tests if a file path has the allowed file extensions.
* @type {RegExp}
*/
get extensionRegExp() {
return internalSlotsMap.get(this).extensionRegExp;
}
/**
* Iterate files which are matched by given glob patterns.
* @param {string|string[]} patternOrPatterns The glob patterns to iterate files.
* @returns {IterableIterator<FileAndConfig>} The found files.
*/
*iterateFiles(patternOrPatterns) {
const { globInputPaths } = internalSlotsMap.get(this);
const patterns = Array.isArray(patternOrPatterns)
? patternOrPatterns
: [patternOrPatterns];
debug("Start to iterate files: %o", patterns);
// The set of paths to remove duplicate.
const set = new Set();
for (const pattern of patterns) {
let foundRegardlessOfIgnored = false;
let found = false;
// Skip empty string.
if (!pattern) {
continue;
}
// Iterate files of this pttern.
for (const { config, filePath, flag } of this._iterateFiles(pattern)) {
foundRegardlessOfIgnored = true;
if (flag === IGNORED_SILENTLY) {
continue;
}
found = true;
// Remove duplicate paths while yielding paths.
if (!set.has(filePath)) {
set.add(filePath);
yield {
config,
filePath,
ignored: flag === IGNORED
};
}
}
// Raise an error if any files were not found.
if (!foundRegardlessOfIgnored) {
throw new NoFilesFoundError(
pattern,
!globInputPaths && isGlob(pattern)
);
}
if (!found) {
throw new AllFilesIgnoredError(pattern);
}
}
debug(`Complete iterating files: ${JSON.stringify(patterns)}`);
}
/**
* Iterate files which are matched by a given glob pattern.
* @param {string} pattern The glob pattern to iterate files.
* @returns {IterableIterator<FileEntry>} The found files.
*/
_iterateFiles(pattern) {
const { cwd, globInputPaths } = internalSlotsMap.get(this);
const absolutePath = path.resolve(cwd, pattern);
const isDot = dotfilesPattern.test(pattern);
const stat = statSafeSync(absolutePath);
if (stat && stat.isDirectory()) {
return this._iterateFilesWithDirectory(absolutePath, isDot);
}
if (stat && stat.isFile()) {
return this._iterateFilesWithFile(absolutePath);
}
if (globInputPaths && isGlobPattern(pattern)) {
return this._iterateFilesWithGlob(absolutePath, isDot);
}
return [];
}
/**
* Iterate a file which is matched by a given path.
* @param {string} filePath The path to the target file.
* @returns {IterableIterator<FileEntry>} The found files.
* @private
*/
_iterateFilesWithFile(filePath) {
debug(`File: ${filePath}`);
const { configArrayFactory } = internalSlotsMap.get(this);
const config = configArrayFactory.getConfigArrayForFile(filePath);
const ignored = this._isIgnoredFile(filePath, { direct: true });
const flag = ignored ? IGNORED : NONE;
return [{ config, filePath, flag }];
}
/**
* Iterate files in a given path.
* @param {string} directoryPath The path to the target directory.
* @param {boolean} dotfiles If `true` then it doesn't skip dot files by default.
* @returns {IterableIterator<FileEntry>} The found files.
* @private
*/
_iterateFilesWithDirectory(directoryPath, dotfiles) {
debug(`Directory: ${directoryPath}`);
return this._iterateFilesRecursive(
directoryPath,
{ dotfiles, recursive: true, selector: null }
);
}
/**
* Iterate files which are matched by a given glob pattern.
* @param {string} pattern The glob pattern to iterate files.
* @param {boolean} dotfiles If `true` then it doesn't skip dot files by default.
* @returns {IterableIterator<FileEntry>} The found files.
* @private
*/
_iterateFilesWithGlob(pattern, dotfiles) {
debug(`Glob: ${pattern}`);
const directoryPath = getGlobParent(pattern);
const globPart = pattern.slice(directoryPath.length + 1);
/*
* recursive if there are `**` or path separators in the glob part.
* Otherwise, patterns such as `src/*.js`, it doesn't need recursive.
*/
const recursive = /\*\*|\/|\\/u.test(globPart);
const selector = new Minimatch(pattern, minimatchOpts);
debug(`recursive? ${recursive}`);
return this._iterateFilesRecursive(
directoryPath,
{ dotfiles, recursive, selector }
);
}
/**
* Iterate files in a given path.
* @param {string} directoryPath The path to the target directory.
* @param {Object} options The options to iterate files.
* @param {boolean} [options.dotfiles] If `true` then it doesn't skip dot files by default.
* @param {boolean} [options.recursive] If `true` then it dives into sub directories.
* @param {InstanceType<Minimatch>} [options.selector] The matcher to choose files.
* @returns {IterableIterator<FileEntry>} The found files.
* @private
*/
*_iterateFilesRecursive(directoryPath, options) {
if (this._isIgnoredFile(directoryPath + path.sep, options)) {
return;
}
debug(`Enter the directory: ${directoryPath}`);
const { configArrayFactory, extensionRegExp } = internalSlotsMap.get(this);
/** @type {ConfigArray|null} */
let config = null;
// Enumerate the files of this directory.
for (const filename of readdirSafeSync(directoryPath)) {
const filePath = path.join(directoryPath, filename);
const stat = statSafeSync(filePath); // TODO: Use `withFileTypes` in the future.
// Check if the file is matched.
if (stat && stat.isFile()) {
if (!config) {
config = configArrayFactory.getConfigArrayForFile(filePath);
}
const ignored = this._isIgnoredFile(filePath, options);
const flag = ignored ? IGNORED_SILENTLY : NONE;
const matched = options.selector
// Started with a glob pattern; choose by the pattern.
? options.selector.match(filePath)
// Started with a directory path; choose by file extensions.
: extensionRegExp.test(filePath);
if (matched) {
debug(`Yield: ${filename}${ignored ? " but ignored" : ""}`);
yield { config, filePath, flag };
} else {
debug(`Didn't match: ${filename}`);
}
// Dive into the sub directory.
} else if (options.recursive && stat && stat.isDirectory()) {
yield* this._iterateFilesRecursive(filePath, options);
}
}
debug(`Leave the directory: ${directoryPath}`);
}
/**
* Check if a given file should be ignored.
* @param {string} filePath The path to a file to check.
* @param {Object} options Options
* @param {boolean} [options.dotfiles] If `true` then this is not ignore dot files by default.
* @param {boolean} [options.direct] If `true` then this is a direct specified file.
* @returns {boolean} `true` if the file should be ignored.
* @private
*/
_isIgnoredFile(filePath, { dotfiles = false, direct = false }) {
const {
ignoreFlag,
ignoredPaths,
ignoredPathsWithDotfiles
} = internalSlotsMap.get(this);
const adoptedIgnoredPaths = dotfiles
? ignoredPathsWithDotfiles
: ignoredPaths;
return ignoreFlag
? adoptedIgnoredPaths.contains(filePath)
: (!direct && adoptedIgnoredPaths.contains(filePath, "default"));
}
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = { FileEnumerator };
+60
View File
@@ -0,0 +1,60 @@
/**
* @fileoverview CheckStyle XML reporter
* @author Ian Christian Myers
*/
"use strict";
const xmlEscape = require("../xml-escape");
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns the severity of warning or error
* @param {Object} message message object to examine
* @returns {string} severity level
* @private
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "error";
}
return "warning";
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "";
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
output += "<checkstyle version=\"4.3\">";
results.forEach(result => {
const messages = result.messages;
output += `<file name="${xmlEscape(result.filePath)}">`;
messages.forEach(message => {
output += [
`<error line="${xmlEscape(message.line)}"`,
`column="${xmlEscape(message.column)}"`,
`severity="${xmlEscape(getMessageType(message))}"`,
`message="${xmlEscape(message.message)}${message.ruleId ? ` (${message.ruleId})` : ""}"`,
`source="${message.ruleId ? xmlEscape(`eslint.rules.${message.ruleId}`) : ""}" />`
].join(" ");
});
output += "</file>";
});
output += "</checkstyle>";
return output;
};
+138
View File
@@ -0,0 +1,138 @@
/**
* @fileoverview Codeframe reporter
* @author Vitor Balocco
*/
"use strict";
const chalk = require("chalk");
const { codeFrameColumns } = require("@babel/code-frame");
const path = require("path");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Given a word and a count, append an s if count is not one.
* @param {string} word A word in its singular form.
* @param {number} count A number controlling whether word should be pluralized.
* @returns {string} The original word with an s on the end if count is not one.
*/
function pluralize(word, count) {
return (count === 1 ? word : `${word}s`);
}
/**
* Gets a formatted relative file path from an absolute path and a line/column in the file.
* @param {string} filePath The absolute file path to format.
* @param {number} line The line from the file to use for formatting.
* @param {number} column The column from the file to use for formatting.
* @returns {string} The formatted file path.
*/
function formatFilePath(filePath, line, column) {
let relPath = path.relative(process.cwd(), filePath);
if (line && column) {
relPath += `:${line}:${column}`;
}
return chalk.green(relPath);
}
/**
* Gets the formatted output for a given message.
* @param {Object} message The object that represents this message.
* @param {Object} parentResult The result object that this message belongs to.
* @returns {string} The formatted output.
*/
function formatMessage(message, parentResult) {
const type = (message.fatal || message.severity === 2) ? chalk.red("error") : chalk.yellow("warning");
const msg = `${chalk.bold(message.message.replace(/([^ ])\.$/u, "$1"))}`;
const ruleId = message.fatal ? "" : chalk.dim(`(${message.ruleId})`);
const filePath = formatFilePath(parentResult.filePath, message.line, message.column);
const sourceCode = parentResult.output ? parentResult.output : parentResult.source;
const firstLine = [
`${type}:`,
`${msg}`,
ruleId ? `${ruleId}` : "",
sourceCode ? `at ${filePath}:` : `at ${filePath}`
].filter(String).join(" ");
const result = [firstLine];
if (sourceCode) {
result.push(
codeFrameColumns(sourceCode, { start: { line: message.line, column: message.column } }, { highlightCode: false })
);
}
return result.join("\n");
}
/**
* Gets the formatted output summary for a given number of errors and warnings.
* @param {number} errors The number of errors.
* @param {number} warnings The number of warnings.
* @param {number} fixableErrors The number of fixable errors.
* @param {number} fixableWarnings The number of fixable warnings.
* @returns {string} The formatted output summary.
*/
function formatSummary(errors, warnings, fixableErrors, fixableWarnings) {
const summaryColor = errors > 0 ? "red" : "yellow";
const summary = [];
const fixablesSummary = [];
if (errors > 0) {
summary.push(`${errors} ${pluralize("error", errors)}`);
}
if (warnings > 0) {
summary.push(`${warnings} ${pluralize("warning", warnings)}`);
}
if (fixableErrors > 0) {
fixablesSummary.push(`${fixableErrors} ${pluralize("error", fixableErrors)}`);
}
if (fixableWarnings > 0) {
fixablesSummary.push(`${fixableWarnings} ${pluralize("warning", fixableWarnings)}`);
}
let output = chalk[summaryColor].bold(`${summary.join(" and ")} found.`);
if (fixableErrors || fixableWarnings) {
output += chalk[summaryColor].bold(`\n${fixablesSummary.join(" and ")} potentially fixable with the \`--fix\` option.`);
}
return output;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let errors = 0;
let warnings = 0;
let fixableErrors = 0;
let fixableWarnings = 0;
const resultsWithMessages = results.filter(result => result.messages.length > 0);
let output = resultsWithMessages.reduce((resultsOutput, result) => {
const messages = result.messages.map(message => `${formatMessage(message, result)}\n\n`);
errors += result.errorCount;
warnings += result.warningCount;
fixableErrors += result.fixableErrorCount;
fixableWarnings += result.fixableWarningCount;
return resultsOutput.concat(messages);
}, []).join("\n");
output += "\n";
output += formatSummary(errors, warnings, fixableErrors, fixableWarnings);
return (errors + warnings) > 0 ? output : "";
};
+60
View File
@@ -0,0 +1,60 @@
/**
* @fileoverview Compact reporter
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns the severity of warning or error
* @param {Object} message message object to examine
* @returns {string} severity level
* @private
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "Error";
}
return "Warning";
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "",
total = 0;
results.forEach(result => {
const messages = result.messages;
total += messages.length;
messages.forEach(message => {
output += `${result.filePath}: `;
output += `line ${message.line || 0}`;
output += `, col ${message.column || 0}`;
output += `, ${getMessageType(message)}`;
output += ` - ${message.message}`;
output += message.ruleId ? ` (${message.ruleId})` : "";
output += "\n";
});
});
if (total > 0) {
output += `\n${total} problem${total !== 1 ? "s" : ""}`;
}
return output;
};
@@ -0,0 +1,8 @@
<tr style="display:none" class="f-<%= parentIndex %>">
<td><%= lineNumber %>:<%= columnNumber %></td>
<td class="clr-<%= severityNumber %>"><%= severityName %></td>
<td><%- message %></td>
<td>
<a href="<%= ruleUrl %>" target="_blank" rel="noopener noreferrer"><%= ruleId %></a>
</td>
</tr>
+115
View File
@@ -0,0 +1,115 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ESLint Report</title>
<style>
body {
font-family:Arial, "Helvetica Neue", Helvetica, sans-serif;
font-size:16px;
font-weight:normal;
margin:0;
padding:0;
color:#333
}
#overview {
padding:20px 30px
}
td, th {
padding:5px 10px
}
h1 {
margin:0
}
table {
margin:30px;
width:calc(100% - 60px);
max-width:1000px;
border-radius:5px;
border:1px solid #ddd;
border-spacing:0px;
}
th {
font-weight:400;
font-size:medium;
text-align:left;
cursor:pointer
}
td.clr-1, td.clr-2, th span {
font-weight:700
}
th span {
float:right;
margin-left:20px
}
th span:after {
content:"";
clear:both;
display:block
}
tr:last-child td {
border-bottom:none
}
tr td:first-child, tr td:last-child {
color:#9da0a4
}
#overview.bg-0, tr.bg-0 th {
color:#468847;
background:#dff0d8;
border-bottom:1px solid #d6e9c6
}
#overview.bg-1, tr.bg-1 th {
color:#f0ad4e;
background:#fcf8e3;
border-bottom:1px solid #fbeed5
}
#overview.bg-2, tr.bg-2 th {
color:#b94a48;
background:#f2dede;
border-bottom:1px solid #eed3d7
}
td {
border-bottom:1px solid #ddd
}
td.clr-1 {
color:#f0ad4e
}
td.clr-2 {
color:#b94a48
}
td a {
color:#3a33d1;
text-decoration:none
}
td a:hover {
color:#272296;
text-decoration:underline
}
</style>
</head>
<body>
<div id="overview" class="bg-<%= reportColor %>">
<h1>ESLint Report</h1>
<div>
<span><%= reportSummary %></span> - Generated on <%= date %>
</div>
</div>
<table>
<tbody>
<%= results %>
</tbody>
</table>
<script type="text/javascript">
var groups = document.querySelectorAll("tr[data-group]");
for (i = 0; i < groups.length; i++) {
groups[i].addEventListener("click", function() {
var inGroup = document.getElementsByClassName(this.getAttribute("data-group"));
this.innerHTML = (this.innerHTML.indexOf("+") > -1) ? this.innerHTML.replace("+", "-") : this.innerHTML.replace("-", "+");
for (var j = 0; j < inGroup.length; j++) {
inGroup[j].style.display = (inGroup[j].style.display !== "none") ? "none" : "table-row";
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,6 @@
<tr class="bg-<%- color %>" data-group="f-<%- index %>">
<th colspan="4">
[+] <%- filePath %>
<span><%- summary %></span>
</th>
</tr>
+139
View File
@@ -0,0 +1,139 @@
/**
* @fileoverview HTML reporter
* @author Julian Laval
*/
"use strict";
const lodash = require("lodash");
const fs = require("fs");
const path = require("path");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const pageTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-page.html"), "utf-8"));
const messageTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-message.html"), "utf-8"));
const resultTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-result.html"), "utf-8"));
/**
* Given a word and a count, append an s if count is not one.
* @param {string} word A word in its singular form.
* @param {int} count A number controlling whether word should be pluralized.
* @returns {string} The original word with an s on the end if count is not one.
*/
function pluralize(word, count) {
return (count === 1 ? word : `${word}s`);
}
/**
* Renders text along the template of x problems (x errors, x warnings)
* @param {string} totalErrors Total errors
* @param {string} totalWarnings Total warnings
* @returns {string} The formatted string, pluralized where necessary
*/
function renderSummary(totalErrors, totalWarnings) {
const totalProblems = totalErrors + totalWarnings;
let renderedText = `${totalProblems} ${pluralize("problem", totalProblems)}`;
if (totalProblems !== 0) {
renderedText += ` (${totalErrors} ${pluralize("error", totalErrors)}, ${totalWarnings} ${pluralize("warning", totalWarnings)})`;
}
return renderedText;
}
/**
* Get the color based on whether there are errors/warnings...
* @param {string} totalErrors Total errors
* @param {string} totalWarnings Total warnings
* @returns {int} The color code (0 = green, 1 = yellow, 2 = red)
*/
function renderColor(totalErrors, totalWarnings) {
if (totalErrors !== 0) {
return 2;
}
if (totalWarnings !== 0) {
return 1;
}
return 0;
}
/**
* Get HTML (table rows) describing the messages.
* @param {Array} messages Messages.
* @param {int} parentIndex Index of the parent HTML row.
* @param {Object} rulesMeta Dictionary containing metadata for each rule executed by the analysis.
* @returns {string} HTML (table rows) describing the messages.
*/
function renderMessages(messages, parentIndex, rulesMeta) {
/**
* Get HTML (table row) describing a message.
* @param {Object} message Message.
* @returns {string} HTML (table row) describing a message.
*/
return lodash.map(messages, message => {
const lineNumber = message.line || 0;
const columnNumber = message.column || 0;
let ruleUrl;
if (rulesMeta) {
const meta = rulesMeta[message.ruleId];
ruleUrl = lodash.get(meta, "docs.url", null);
}
return messageTemplate({
parentIndex,
lineNumber,
columnNumber,
severityNumber: message.severity,
severityName: message.severity === 1 ? "Warning" : "Error",
message: message.message,
ruleId: message.ruleId,
ruleUrl
});
}).join("\n");
}
/**
* @param {Array} results Test results.
* @param {Object} rulesMeta Dictionary containing metadata for each rule executed by the analysis.
* @returns {string} HTML string describing the results.
*/
function renderResults(results, rulesMeta) {
return lodash.map(results, (result, index) => resultTemplate({
index,
color: renderColor(result.errorCount, result.warningCount),
filePath: result.filePath,
summary: renderSummary(result.errorCount, result.warningCount)
}) + renderMessages(result.messages, index, rulesMeta)).join("\n");
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results, data) {
let totalErrors,
totalWarnings;
const metaData = data ? data.rulesMeta : {};
totalErrors = 0;
totalWarnings = 0;
// Iterate over results to get totals
results.forEach(result => {
totalErrors += result.errorCount;
totalWarnings += result.warningCount;
});
return pageTemplate({
date: new Date(),
reportColor: renderColor(totalErrors, totalWarnings),
reportSummary: renderSummary(totalErrors, totalWarnings),
results: renderResults(results, metaData)
});
};
+41
View File
@@ -0,0 +1,41 @@
/**
* @fileoverview JSLint XML reporter
* @author Ian Christian Myers
*/
"use strict";
const xmlEscape = require("../xml-escape");
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "";
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
output += "<jslint>";
results.forEach(result => {
const messages = result.messages;
output += `<file name="${result.filePath}">`;
messages.forEach(message => {
output += [
`<issue line="${message.line}"`,
`char="${message.column}"`,
`evidence="${xmlEscape(message.source || "")}"`,
`reason="${xmlEscape(message.message || "")}${message.ruleId ? ` (${message.ruleId})` : ""}" />`
].join(" ");
});
output += "</file>";
});
output += "</jslint>";
return output;
};
+16
View File
@@ -0,0 +1,16 @@
/**
* @fileoverview JSON reporter, including rules metadata
* @author Chris Meyer
*/
"use strict";
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results, data) {
return JSON.stringify({
results,
metadata: data
});
};
+13
View File
@@ -0,0 +1,13 @@
/**
* @fileoverview JSON reporter
* @author Burak Yigit Kaya aka BYK
*/
"use strict";
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
return JSON.stringify(results);
};
+82
View File
@@ -0,0 +1,82 @@
/**
* @fileoverview jUnit Reporter
* @author Jamund Ferguson
*/
"use strict";
const xmlEscape = require("../xml-escape");
const path = require("path");
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns the severity of warning or error
* @param {Object} message message object to examine
* @returns {string} severity level
* @private
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "Error";
}
return "Warning";
}
/**
* Returns a full file path without extension
* @param {string} filePath input file path
* @returns {string} file path without extension
* @private
*/
function pathWithoutExt(filePath) {
return path.posix.join(path.posix.dirname(filePath), path.basename(filePath, path.extname(filePath)));
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "";
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
output += "<testsuites>\n";
results.forEach(result => {
const messages = result.messages;
const classname = pathWithoutExt(result.filePath);
if (messages.length > 0) {
output += `<testsuite package="org.eslint" time="0" tests="${messages.length}" errors="${messages.length}" name="${result.filePath}">\n`;
messages.forEach(message => {
const type = message.fatal ? "error" : "failure";
output += `<testcase time="0" name="org.eslint.${message.ruleId || "unknown"}" classname="${classname}">`;
output += `<${type} message="${xmlEscape(message.message || "")}">`;
output += "<![CDATA[";
output += `line ${message.line || 0}, col `;
output += `${message.column || 0}, ${getMessageType(message)}`;
output += ` - ${xmlEscape(message.message || "")}`;
output += (message.ruleId ? ` (${message.ruleId})` : "");
output += "]]>";
output += `</${type}>`;
output += "</testcase>\n";
});
output += "</testsuite>\n";
} else {
output += `<testsuite package="org.eslint" time="0" tests="1" errors="0" name="${result.filePath}">\n`;
output += `<testcase time="0" name="${result.filePath}" classname="${classname}" />\n`;
output += "</testsuite>\n";
}
});
output += "</testsuites>\n";
return output;
};
+101
View File
@@ -0,0 +1,101 @@
/**
* @fileoverview Stylish reporter
* @author Sindre Sorhus
*/
"use strict";
const chalk = require("chalk"),
stripAnsi = require("strip-ansi"),
table = require("text-table");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Given a word and a count, append an s if count is not one.
* @param {string} word A word in its singular form.
* @param {int} count A number controlling whether word should be pluralized.
* @returns {string} The original word with an s on the end if count is not one.
*/
function pluralize(word, count) {
return (count === 1 ? word : `${word}s`);
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "\n",
errorCount = 0,
warningCount = 0,
fixableErrorCount = 0,
fixableWarningCount = 0,
summaryColor = "yellow";
results.forEach(result => {
const messages = result.messages;
if (messages.length === 0) {
return;
}
errorCount += result.errorCount;
warningCount += result.warningCount;
fixableErrorCount += result.fixableErrorCount;
fixableWarningCount += result.fixableWarningCount;
output += `${chalk.underline(result.filePath)}\n`;
output += `${table(
messages.map(message => {
let messageType;
if (message.fatal || message.severity === 2) {
messageType = chalk.red("error");
summaryColor = "red";
} else {
messageType = chalk.yellow("warning");
}
return [
"",
message.line || 0,
message.column || 0,
messageType,
message.message.replace(/([^ ])\.$/u, "$1"),
chalk.dim(message.ruleId || "")
];
}),
{
align: ["", "r", "l"],
stringLength(str) {
return stripAnsi(str).length;
}
}
).split("\n").map(el => el.replace(/(\d+)\s+(\d+)/u, (m, p1, p2) => chalk.dim(`${p1}:${p2}`))).join("\n")}\n\n`;
});
const total = errorCount + warningCount;
if (total > 0) {
output += chalk[summaryColor].bold([
"\u2716 ", total, pluralize(" problem", total),
" (", errorCount, pluralize(" error", errorCount), ", ",
warningCount, pluralize(" warning", warningCount), ")\n"
].join(""));
if (fixableErrorCount > 0 || fixableWarningCount > 0) {
output += chalk[summaryColor].bold([
" ", fixableErrorCount, pluralize(" error", fixableErrorCount), " and ",
fixableWarningCount, pluralize(" warning", fixableWarningCount),
" potentially fixable with the `--fix` option.\n"
].join(""));
}
}
// Resets output color, for prevent change on top level
return total > 0 ? chalk.reset(output) : "";
};
+159
View File
@@ -0,0 +1,159 @@
/**
* @fileoverview "table reporter.
* @author Gajus Kuizinas <gajus@gajus.com>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const chalk = require("chalk"),
table = require("table").table;
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Given a word and a count, append an "s" if count is not one.
* @param {string} word A word.
* @param {number} count Quantity.
* @returns {string} The original word with an s on the end if count is not one.
*/
function pluralize(word, count) {
return (count === 1 ? word : `${word}s`);
}
/**
* Draws text table.
* @param {Array<Object>} messages Error messages relating to a specific file.
* @returns {string} A text table.
*/
function drawTable(messages) {
const rows = [];
if (messages.length === 0) {
return "";
}
rows.push([
chalk.bold("Line"),
chalk.bold("Column"),
chalk.bold("Type"),
chalk.bold("Message"),
chalk.bold("Rule ID")
]);
messages.forEach(message => {
let messageType;
if (message.fatal || message.severity === 2) {
messageType = chalk.red("error");
} else {
messageType = chalk.yellow("warning");
}
rows.push([
message.line || 0,
message.column || 0,
messageType,
message.message,
message.ruleId || ""
]);
});
return table(rows, {
columns: {
0: {
width: 8,
wrapWord: true
},
1: {
width: 8,
wrapWord: true
},
2: {
width: 8,
wrapWord: true
},
3: {
paddingRight: 5,
width: 50,
wrapWord: true
},
4: {
width: 20,
wrapWord: true
}
},
drawHorizontalLine(index) {
return index === 1;
}
});
}
/**
* Draws a report (multiple tables).
* @param {Array} results Report results for every file.
* @returns {string} A column of text tables.
*/
function drawReport(results) {
let files;
files = results.map(result => {
if (!result.messages.length) {
return "";
}
return `\n${result.filePath}\n\n${drawTable(result.messages)}`;
});
files = files.filter(content => content.trim());
return files.join("");
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(report) {
let result,
errorCount,
warningCount;
result = "";
errorCount = 0;
warningCount = 0;
report.forEach(fileReport => {
errorCount += fileReport.errorCount;
warningCount += fileReport.warningCount;
});
if (errorCount || warningCount) {
result = drawReport(report);
}
result += `\n${table([
[
chalk.red(pluralize(`${errorCount} Error`, errorCount))
],
[
chalk.yellow(pluralize(`${warningCount} Warning`, warningCount))
]
], {
columns: {
0: {
width: 110,
wrapWord: true
}
},
drawHorizontalLine() {
return true;
}
})}`;
return result;
};
+95
View File
@@ -0,0 +1,95 @@
/**
* @fileoverview TAP reporter
* @author Jonathan Kingston
*/
"use strict";
const yaml = require("js-yaml");
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns a canonical error level string based upon the error message passed in.
* @param {Object} message Individual error message provided by eslint
* @returns {string} Error level string
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "error";
}
return "warning";
}
/**
* Takes in a JavaScript object and outputs a TAP diagnostics string
* @param {Object} diagnostic JavaScript object to be embedded as YAML into output.
* @returns {string} diagnostics string with YAML embedded - TAP version 13 compliant
*/
function outputDiagnostics(diagnostic) {
const prefix = " ";
let output = `${prefix}---\n`;
output += prefix + yaml.safeDump(diagnostic).split("\n").join(`\n${prefix}`);
output += "...\n";
return output;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = `TAP version 13\n1..${results.length}\n`;
results.forEach((result, id) => {
const messages = result.messages;
let testResult = "ok";
let diagnostics = {};
if (messages.length > 0) {
messages.forEach(message => {
const severity = getMessageType(message);
const diagnostic = {
message: message.message,
severity,
data: {
line: message.line || 0,
column: message.column || 0,
ruleId: message.ruleId || ""
}
};
// This ensures a warning message is not flagged as error
if (severity === "error") {
testResult = "not ok";
}
/*
* If we have multiple messages place them under a messages key
* The first error will be logged as message key
* This is to adhere to TAP 13 loosely defined specification of having a message key
*/
if ("message" in diagnostics) {
if (typeof diagnostics.messages === "undefined") {
diagnostics.messages = [];
}
diagnostics.messages.push(diagnostic);
} else {
diagnostics = diagnostic;
}
});
}
output += `${testResult} ${id + 1} - ${result.filePath}\n`;
// If we have an error include diagnostics
if (messages.length > 0) {
output += outputDiagnostics(diagnostics);
}
});
return output;
};
+58
View File
@@ -0,0 +1,58 @@
/**
* @fileoverview unix-style formatter.
* @author oshi-shinobu
*/
"use strict";
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns a canonical error level string based upon the error message passed in.
* @param {Object} message Individual error message provided by eslint
* @returns {string} Error level string
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "Error";
}
return "Warning";
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "",
total = 0;
results.forEach(result => {
const messages = result.messages;
total += messages.length;
messages.forEach(message => {
output += `${result.filePath}:`;
output += `${message.line || 0}:`;
output += `${message.column || 0}:`;
output += ` ${message.message} `;
output += `[${getMessageType(message)}${message.ruleId ? `/${message.ruleId}` : ""}]`;
output += "\n";
});
});
if (total > 0) {
output += `\n${total} problem${total !== 1 ? "s" : ""}`;
}
return output;
};
+63
View File
@@ -0,0 +1,63 @@
/**
* @fileoverview Visual Studio compatible formatter
* @author Ronald Pijnacker
*/
"use strict";
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns the severity of warning or error
* @param {Object} message message object to examine
* @returns {string} severity level
* @private
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "error";
}
return "warning";
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "",
total = 0;
results.forEach(result => {
const messages = result.messages;
total += messages.length;
messages.forEach(message => {
output += result.filePath;
output += `(${message.line || 0}`;
output += message.column ? `,${message.column}` : "";
output += `): ${getMessageType(message)}`;
output += message.ruleId ? ` ${message.ruleId}` : "";
output += ` : ${message.message}`;
output += "\n";
});
});
if (total === 0) {
output += "no problems";
} else {
output += `\n${total} problem${total !== 1 ? "s" : ""}`;
}
return output;
};
+35
View File
@@ -0,0 +1,35 @@
/**
* @fileoverview Defining the hashing function in one place.
* @author Michael Ficarra
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const murmur = require("imurmurhash");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
/**
* hash the given string
* @param {string} str the string to hash
* @returns {string} the hash
*/
function hash(str) {
return murmur(str).result().toString(36);
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = hash;
+362
View File
@@ -0,0 +1,362 @@
/**
* @fileoverview Responsible for loading ignore config files and managing ignore patterns
* @author Jonathan Rajavuori
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs"),
path = require("path"),
ignore = require("ignore");
const debug = require("debug")("eslint:ignored-paths");
//------------------------------------------------------------------------------
// Constants
//------------------------------------------------------------------------------
const ESLINT_IGNORE_FILENAME = ".eslintignore";
/**
* Adds `"*"` at the end of `"node_modules/"`,
* so that subtle directories could be re-included by .gitignore patterns
* such as `"!node_modules/should_not_ignored"`
*/
const DEFAULT_IGNORE_DIRS = [
"/node_modules/*",
"/bower_components/*"
];
const DEFAULT_OPTIONS = {
dotfiles: false,
cwd: process.cwd()
};
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Find a file in the current directory.
* @param {string} cwd Current working directory
* @param {string} name File name
* @returns {string} Path of ignore file or an empty string.
*/
function findFile(cwd, name) {
const ignoreFilePath = path.resolve(cwd, name);
return fs.existsSync(ignoreFilePath) && fs.statSync(ignoreFilePath).isFile() ? ignoreFilePath : "";
}
/**
* Find an ignore file in the current directory.
* @param {string} cwd Current working directory
* @returns {string} Path of ignore file or an empty string.
*/
function findIgnoreFile(cwd) {
return findFile(cwd, ESLINT_IGNORE_FILENAME);
}
/**
* Find an package.json file in the current directory.
* @param {string} cwd Current working directory
* @returns {string} Path of package.json file or an empty string.
*/
function findPackageJSONFile(cwd) {
return findFile(cwd, "package.json");
}
/**
* Merge options with defaults
* @param {Object} options Options to merge with DEFAULT_OPTIONS constant
* @returns {Object} Merged options
*/
function mergeDefaultOptions(options) {
return Object.assign({}, DEFAULT_OPTIONS, options);
}
/* eslint-disable jsdoc/check-param-names, jsdoc/require-param */
/**
* Normalize the path separators in a given string.
* On Windows environment, this replaces `\` by `/`.
* Otherwrise, this does nothing.
* @param {string} str The path string to normalize.
* @returns {string} The normalized path.
*/
const normalizePathSeps = path.sep === "/"
? (str => str)
: ((seps, str) => str.replace(seps, "/")).bind(null, new RegExp(`\\${path.sep}`, "gu"));
/* eslint-enable jsdoc/check-param-names, jsdoc/require-param */
/**
* Converts a glob pattern to a new glob pattern relative to a different directory
* @param {string} globPattern The glob pattern, relative the the old base directory
* @param {string} relativePathToOldBaseDir A relative path from the new base directory to the old one
* @returns {string} A glob pattern relative to the new base directory
*/
function relativize(globPattern, relativePathToOldBaseDir) {
if (relativePathToOldBaseDir === "") {
return globPattern;
}
const prefix = globPattern.startsWith("!") ? "!" : "";
const globWithoutPrefix = globPattern.replace(/^!/u, "");
if (globWithoutPrefix.startsWith("/")) {
return `${prefix}/${normalizePathSeps(relativePathToOldBaseDir)}${globWithoutPrefix}`;
}
return globPattern;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* IgnoredPaths class
*/
class IgnoredPaths {
/**
* @param {Object} providedOptions object containing 'ignore', 'ignorePath' and 'patterns' properties
*/
constructor(providedOptions) {
const options = mergeDefaultOptions(providedOptions);
this.cache = {};
this.defaultPatterns = [].concat(DEFAULT_IGNORE_DIRS, options.patterns || []);
this.ignoreFileDir = options.ignore !== false && options.ignorePath
? path.dirname(path.resolve(options.cwd, options.ignorePath))
: options.cwd;
this.options = options;
this._baseDir = null;
this.ig = {
custom: ignore(),
default: ignore()
};
this.defaultPatterns.forEach(pattern => this.addPatternRelativeToCwd(this.ig.default, pattern));
if (options.dotfiles !== true) {
/*
* ignore files beginning with a dot, but not files in a parent or
* ancestor directory (which in relative format will begin with `../`).
*/
this.addPatternRelativeToCwd(this.ig.default, ".*");
this.addPatternRelativeToCwd(this.ig.default, "!../");
}
/*
* Add a way to keep track of ignored files. This was present in node-ignore
* 2.x, but dropped for now as of 3.0.10.
*/
this.ig.custom.ignoreFiles = [];
this.ig.default.ignoreFiles = [];
if (options.ignore !== false) {
let ignorePath;
if (options.ignorePath) {
debug("Using specific ignore file");
try {
const stat = fs.statSync(options.ignorePath);
if (!stat.isFile()) {
throw new Error(`${options.ignorePath} is not a file`);
}
ignorePath = options.ignorePath;
} catch (e) {
e.message = `Cannot read ignore file: ${options.ignorePath}\nError: ${e.message}`;
throw e;
}
} else {
debug(`Looking for ignore file in ${options.cwd}`);
ignorePath = findIgnoreFile(options.cwd);
try {
fs.statSync(ignorePath);
debug(`Loaded ignore file ${ignorePath}`);
} catch (e) {
debug("Could not find ignore file in cwd");
}
}
if (ignorePath) {
debug(`Adding ${ignorePath}`);
this.addIgnoreFile(this.ig.custom, ignorePath);
this.addIgnoreFile(this.ig.default, ignorePath);
} else {
try {
// if the ignoreFile does not exist, check package.json for eslintIgnore
const packageJSONPath = findPackageJSONFile(options.cwd);
if (packageJSONPath) {
let packageJSONOptions;
try {
packageJSONOptions = JSON.parse(fs.readFileSync(packageJSONPath, "utf8"));
} catch (e) {
debug("Could not read package.json file to check eslintIgnore property");
e.messageTemplate = "failed-to-read-json";
e.messageData = {
path: packageJSONPath,
message: e.message
};
throw e;
}
if (packageJSONOptions.eslintIgnore) {
if (Array.isArray(packageJSONOptions.eslintIgnore)) {
packageJSONOptions.eslintIgnore.forEach(pattern => {
this.addPatternRelativeToIgnoreFile(this.ig.custom, pattern);
this.addPatternRelativeToIgnoreFile(this.ig.default, pattern);
});
} else {
throw new TypeError("Package.json eslintIgnore property requires an array of paths");
}
}
}
} catch (e) {
debug("Could not find package.json to check eslintIgnore property");
throw e;
}
}
if (options.ignorePattern) {
this.addPatternRelativeToCwd(this.ig.custom, options.ignorePattern);
this.addPatternRelativeToCwd(this.ig.default, options.ignorePattern);
}
}
}
/*
* If `ignoreFileDir` is a subdirectory of `cwd`, all paths will be normalized to be relative to `cwd`.
* Otherwise, all paths will be normalized to be relative to `ignoreFileDir`.
* This ensures that the final normalized ignore rule will not contain `..`, which is forbidden in
* ignore rules.
*/
addPatternRelativeToCwd(ig, pattern) {
const baseDir = this.getBaseDir();
const cookedPattern = baseDir === this.options.cwd
? pattern
: relativize(pattern, path.relative(baseDir, this.options.cwd));
ig.addPattern(cookedPattern);
debug("addPatternRelativeToCwd:\n original = %j\n cooked = %j", pattern, cookedPattern);
}
addPatternRelativeToIgnoreFile(ig, pattern) {
const baseDir = this.getBaseDir();
const cookedPattern = baseDir === this.ignoreFileDir
? pattern
: relativize(pattern, path.relative(baseDir, this.ignoreFileDir));
ig.addPattern(cookedPattern);
debug("addPatternRelativeToIgnoreFile:\n original = %j\n cooked = %j", pattern, cookedPattern);
}
// Detect the common ancestor
getBaseDir() {
if (!this._baseDir) {
const a = path.resolve(this.options.cwd);
const b = path.resolve(this.ignoreFileDir);
let lastSepPos = 0;
// Set the shorter one (it's the common ancestor if one includes the other).
this._baseDir = a.length < b.length ? a : b;
// Set the common ancestor.
for (let i = 0; i < a.length && i < b.length; ++i) {
if (a[i] !== b[i]) {
this._baseDir = a.slice(0, lastSepPos);
break;
}
if (a[i] === path.sep) {
lastSepPos = i;
}
}
// If it's only Windows drive letter, it needs \
if (/^[A-Z]:$/u.test(this._baseDir)) {
this._baseDir += "\\";
}
debug("baseDir = %j", this._baseDir);
}
return this._baseDir;
}
/**
* read ignore filepath
* @param {string} filePath file to add to ig
* @returns {Array} raw ignore rules
*/
readIgnoreFile(filePath) {
if (typeof this.cache[filePath] === "undefined") {
this.cache[filePath] = fs.readFileSync(filePath, "utf8").split(/\r?\n/gu).filter(Boolean);
}
return this.cache[filePath];
}
/**
* add ignore file to node-ignore instance
* @param {Object} ig instance of node-ignore
* @param {string} filePath file to add to ig
* @returns {void}
*/
addIgnoreFile(ig, filePath) {
ig.ignoreFiles.push(filePath);
this
.readIgnoreFile(filePath)
.forEach(ignoreRule => this.addPatternRelativeToIgnoreFile(ig, ignoreRule));
}
/**
* Determine whether a file path is included in the default or custom ignore patterns
* @param {string} filepath Path to check
* @param {string} [category=undefined] check 'default', 'custom' or both (undefined)
* @returns {boolean} true if the file path matches one or more patterns, false otherwise
*/
contains(filepath, category) {
const isDir = filepath.endsWith(path.sep) ||
(path.sep === "\\" && filepath.endsWith("/"));
let result = false;
const basePath = this.getBaseDir();
const absolutePath = path.resolve(this.options.cwd, filepath);
let relativePath = path.relative(basePath, absolutePath);
if (relativePath) {
if (isDir) {
relativePath += path.sep;
}
if (typeof category === "undefined") {
result =
(this.ig.default.filter([relativePath]).length === 0) ||
(this.ig.custom.filter([relativePath]).length === 0);
} else {
result =
(this.ig[category].filter([relativePath]).length === 0);
}
}
debug("contains:");
debug(" target = %j", filepath);
debug(" base = %j", basePath);
debug(" relative = %j", relativePath);
debug(" result = %j", result);
return result;
}
}
module.exports = { IgnoredPaths };
+7
View File
@@ -0,0 +1,7 @@
"use strict";
const { CLIEngine } = require("./cli-engine");
module.exports = {
CLIEngine
};
+141
View File
@@ -0,0 +1,141 @@
/**
* @fileoverview Utility for caching lint results.
* @author Kevin Partington
*/
"use strict";
//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------
const assert = require("assert");
const fs = require("fs");
const fileEntryCache = require("file-entry-cache");
const stringify = require("json-stable-stringify-without-jsonify");
const pkg = require("../../package.json");
const hash = require("./hash");
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const configHashCache = new WeakMap();
/**
* Calculates the hash of the config
* @param {ConfigArray} config The config.
* @returns {string} The hash of the config
*/
function hashOfConfigFor(config) {
if (!configHashCache.has(config)) {
configHashCache.set(config, hash(`${pkg.version}_${stringify(config)}`));
}
return configHashCache.get(config);
}
//-----------------------------------------------------------------------------
// Public Interface
//-----------------------------------------------------------------------------
/**
* Lint result cache. This wraps around the file-entry-cache module,
* transparently removing properties that are difficult or expensive to
* serialize and adding them back in on retrieval.
*/
class LintResultCache {
/**
* Creates a new LintResultCache instance.
* @param {string} cacheFileLocation The cache file location.
* configuration lookup by file path).
*/
constructor(cacheFileLocation) {
assert(cacheFileLocation, "Cache file location is required");
this.fileEntryCache = fileEntryCache.create(cacheFileLocation);
}
/**
* Retrieve cached lint results for a given file path, if present in the
* cache. If the file is present and has not been changed, rebuild any
* missing result information.
* @param {string} filePath The file for which to retrieve lint results.
* @param {ConfigArray} config The config of the file.
* @returns {Object|null} The rebuilt lint results, or null if the file is
* changed or not in the filesystem.
*/
getCachedLintResults(filePath, config) {
/*
* Cached lint results are valid if and only if:
* 1. The file is present in the filesystem
* 2. The file has not changed since the time it was previously linted
* 3. The ESLint configuration has not changed since the time the file
* was previously linted
* If any of these are not true, we will not reuse the lint results.
*/
const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath);
const hashOfConfig = hashOfConfigFor(config);
const changed = fileDescriptor.changed || fileDescriptor.meta.hashOfConfig !== hashOfConfig;
if (fileDescriptor.notFound || changed) {
return null;
}
// If source is present but null, need to reread the file from the filesystem.
if (fileDescriptor.meta.results && fileDescriptor.meta.results.source === null) {
fileDescriptor.meta.results.source = fs.readFileSync(filePath, "utf-8");
}
return fileDescriptor.meta.results;
}
/**
* Set the cached lint results for a given file path, after removing any
* information that will be both unnecessary and difficult to serialize.
* Avoids caching results with an "output" property (meaning fixes were
* applied), to prevent potentially incorrect results if fixes are not
* written to disk.
* @param {string} filePath The file for which to set lint results.
* @param {ConfigArray} config The config of the file.
* @param {Object} result The lint result to be set for the file.
* @returns {void}
*/
setCachedLintResults(filePath, config, result) {
if (result && Object.prototype.hasOwnProperty.call(result, "output")) {
return;
}
const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath);
if (fileDescriptor && !fileDescriptor.notFound) {
// Serialize the result, except that we want to remove the file source if present.
const resultToSerialize = Object.assign({}, result);
/*
* Set result.source to null.
* In `getCachedLintResults`, if source is explicitly null, we will
* read the file from the filesystem to set the value again.
*/
if (Object.prototype.hasOwnProperty.call(resultToSerialize, "source")) {
resultToSerialize.source = null;
}
fileDescriptor.meta.results = resultToSerialize;
fileDescriptor.meta.hashOfConfig = hashOfConfigFor(config);
}
}
/**
* Persists the in-memory cache to disk.
* @returns {void}
*/
reconcile() {
this.fileEntryCache.reconcile();
}
}
module.exports = LintResultCache;
+46
View File
@@ -0,0 +1,46 @@
/**
* @fileoverview Module for loading rules from files and directories.
* @author Michael Ficarra
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs"),
path = require("path");
const rulesDirCache = {};
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* Load all rule modules from specified directory.
* @param {string} relativeRulesDir Path to rules directory, may be relative.
* @param {string} cwd Current working directory
* @returns {Object} Loaded rule modules.
*/
module.exports = function(relativeRulesDir, cwd) {
const rulesDir = path.resolve(cwd, relativeRulesDir);
// cache will help performance as IO operation are expensive
if (rulesDirCache[rulesDir]) {
return rulesDirCache[rulesDir];
}
const rules = Object.create(null);
fs.readdirSync(rulesDir).forEach(file => {
if (path.extname(file) !== ".js") {
return;
}
rules[file.slice(0, -3)] = require(path.join(rulesDir, file));
});
rulesDirCache[rulesDir] = rules;
return rules;
};
+34
View File
@@ -0,0 +1,34 @@
/**
* @fileoverview XML character escaper
* @author George Chung
*/
"use strict";
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* Returns the escaped value for a character
* @param {string} s string to examine
* @returns {string} severity level
* @private
*/
module.exports = function(s) {
return (`${s}`).replace(/[<>&"'\x00-\x1F\x7F\u0080-\uFFFF]/gu, c => { // eslint-disable-line no-control-regex
switch (c) {
case "<":
return "&lt;";
case ">":
return "&gt;";
case "&":
return "&amp;";
case "\"":
return "&quot;";
case "'":
return "&apos;";
default:
return `&#${c.charCodeAt(0)};`;
}
});
};