| /** |
| * @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 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 naming = require("./naming"); |
| |
| 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. |
| * @param {Map<string, Object>} ruleMap The rule definitions which were used (built-ins). |
| * @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 |
| */ |
| 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) { |
| if (!error || error.code !== "ENOENT") { |
| 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); |
| } |
| }; |