init source
This commit is contained in:
+417
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
};
|
||||
+8
@@ -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
@@ -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>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<tr class="bg-<%- color %>" data-group="f-<%- index %>">
|
||||
<th colspan="4">
|
||||
[+] <%- filePath %>
|
||||
<span><%- summary %></span>
|
||||
</th>
|
||||
</tr>
|
||||
+139
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const { CLIEngine } = require("./cli-engine");
|
||||
|
||||
module.exports = {
|
||||
CLIEngine
|
||||
};
|
||||
+141
@@ -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
@@ -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
@@ -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 "<";
|
||||
case ">":
|
||||
return ">";
|
||||
case "&":
|
||||
return "&";
|
||||
case "\"":
|
||||
return """;
|
||||
case "'":
|
||||
return "'";
|
||||
default:
|
||||
return `&#${c.charCodeAt(0)};`;
|
||||
}
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user