| /** |
| * @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"), |
| path = require("path"), |
| defaultOptions = require("../conf/default-cli-options"), |
| Linter = require("./linter"), |
| IgnoredPaths = require("./ignored-paths"), |
| Config = require("./config"), |
| fileEntryCache = require("file-entry-cache"), |
| globUtil = require("./util/glob-util"), |
| validator = require("./config/config-validator"), |
| stringify = require("json-stable-stringify-without-jsonify"), |
| hash = require("./util/hash"), |
| ModuleResolver = require("./util/module-resolver"), |
| naming = require("./util/naming"), |
| pkg = require("../package.json"); |
| |
| const debug = require("debug")("eslint:cli-engine"); |
| |
| const resolver = new ModuleResolver(); |
| |
| //------------------------------------------------------------------------------ |
| // Typedefs |
| //------------------------------------------------------------------------------ |
| |
| /** |
| * The options to configure a CLI engine with. |
| * @typedef {Object} CLIEngineOptions |
| * @property {boolean} allowInlineConfig Enable or disable inline configuration comments. |
| * @property {boolean|Object} baseConfig Base config object. True enables recommend rules and environments. |
| * @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[]} 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 {Object} parserOptions An object of parserOption settings to use. |
| * @property {string[]} plugins An array of plugins to load. |
| * @property {Object<string,*>} 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 |
| */ |
| |
| /** |
| * A linting warning or error. |
| * @typedef {Object} LintMessage |
| * @property {string} message The message to display to the user. |
| */ |
| |
| /** |
| * 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. |
| */ |
| |
| //------------------------------------------------------------------------------ |
| // Helpers |
| //------------------------------------------------------------------------------ |
| |
| /** |
| * It will calculate the error and warning count for collection of messages per file |
| * @param {Object[]} 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 {Object[]} 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 {string} text The source code to check. |
| * @param {Object} configHelper The configuration options for ESLint. |
| * @param {string} filename An optional string representing the texts filename. |
| * @param {boolean|Function} fix Indicates if fixes should be processed. |
| * @param {boolean} allowInlineConfig Allow/ignore comments that change config. |
| * @param {boolean} reportUnusedDisableDirectives Allow/ignore comments that change config. |
| * @param {Linter} linter Linter context |
| * @returns {LintResult} The results for linting on this text. |
| * @private |
| */ |
| function processText(text, configHelper, filename, fix, allowInlineConfig, reportUnusedDisableDirectives, linter) { |
| let filePath, |
| fileExtension, |
| processor; |
| |
| if (filename) { |
| filePath = path.resolve(filename); |
| fileExtension = path.extname(filename); |
| } |
| |
| const effectiveFilename = filename || "<text>"; |
| |
| debug(`Linting ${effectiveFilename}`); |
| const config = configHelper.getConfig(filePath); |
| |
| if (config.plugins) { |
| configHelper.plugins.loadAll(config.plugins); |
| } |
| |
| const loadedPlugins = configHelper.plugins.getAll(); |
| |
| for (const plugin in loadedPlugins) { |
| if (loadedPlugins[plugin].processors && Object.keys(loadedPlugins[plugin].processors).indexOf(fileExtension) >= 0) { |
| processor = loadedPlugins[plugin].processors[fileExtension]; |
| break; |
| } |
| } |
| |
| const autofixingEnabled = typeof fix !== "undefined" && (!processor || processor.supportsAutofix); |
| |
| const fixedResult = linter.verifyAndFix(text, config, { |
| filename: effectiveFilename, |
| allowInlineConfig, |
| reportUnusedDisableDirectives, |
| fix: !!autofixingEnabled && fix, |
| preprocess: processor && (rawText => processor.preprocess(rawText, effectiveFilename)), |
| postprocess: processor && (problemLists => processor.postprocess(problemLists, effectiveFilename)) |
| }); |
| |
| const stats = calculateStatsPerFile(fixedResult.messages); |
| |
| const result = { |
| filePath: effectiveFilename, |
| messages: fixedResult.messages, |
| errorCount: stats.errorCount, |
| warningCount: stats.warningCount, |
| fixableErrorCount: stats.fixableErrorCount, |
| fixableWarningCount: stats.fixableWarningCount |
| }; |
| |
| if (fixedResult.fixed) { |
| result.output = fixedResult.output; |
| } |
| |
| if (result.errorCount + result.warningCount > 0 && typeof result.output === "undefined") { |
| result.source = text; |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Processes an individual file using ESLint. Files used here are known to |
| * exist, so no need to check that here. |
| * @param {string} filename The filename of the file being checked. |
| * @param {Object} configHelper The configuration options for ESLint. |
| * @param {Object} options The CLIEngine options object. |
| * @param {Linter} linter Linter context |
| * @returns {LintResult} The results for linting on this file. |
| * @private |
| */ |
| function processFile(filename, configHelper, options, linter) { |
| |
| const text = fs.readFileSync(path.resolve(filename), "utf8"), |
| result = processText( |
| text, |
| configHelper, |
| filename, |
| options.fix, |
| options.allowInlineConfig, |
| options.reportUnusedDisableDirectives, |
| linter |
| ); |
| |
| 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 = /^\./.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 |
| }; |
| } |
| |
| |
| /** |
| * Checks if the given message is an error message. |
| * @param {Object} 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; |
| } |
| |
| const configHashCache = new WeakMap(); |
| |
| //------------------------------------------------------------------------------ |
| // 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 |
| ); |
| |
| /** |
| * Stored options for this instance |
| * @type {Object} |
| */ |
| this.options = options; |
| this.linter = new Linter(); |
| |
| if (options.cache) { |
| const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd); |
| |
| /** |
| * Cache used to avoid operating on files that haven't changed since the |
| * last successful execution (e.g., file passed linting with no errors and |
| * no warnings). |
| * @type {Object} |
| */ |
| this._fileCache = fileEntryCache.create(cacheFile); |
| } |
| |
| // load in additional rules |
| if (this.options.rulePaths) { |
| const cwd = this.options.cwd; |
| |
| this.options.rulePaths.forEach(rulesdir => { |
| debug(`Loading rules from ${rulesdir}`); |
| this.linter.rules.load(rulesdir, cwd); |
| }); |
| } |
| |
| if (this.options.rules && Object.keys(this.options.rules).length) { |
| const loadedRules = this.linter.getRules(); |
| |
| Object.keys(this.options.rules).forEach(name => { |
| validator.validateRuleOptions(loadedRules.get(name), name, this.options.rules[name], "CLI"); |
| }); |
| } |
| |
| this.config = new Config(this.options, this.linter); |
| } |
| |
| getRules() { |
| return this.linter.getRules(); |
| } |
| |
| /** |
| * 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( |
| Object.assign(result, { |
| messages: filteredMessages, |
| errorCount: filteredMessages.length, |
| warningCount: 0, |
| fixableErrorCount: result.fixableErrorCount, |
| fixableWarningCount: 0 |
| }) |
| ); |
| } |
| }); |
| |
| return filtered; |
| } |
| |
| /** |
| * Outputs fixes from the given results to files. |
| * @param {Object} report The report object created by CLIEngine. |
| * @returns {void} |
| */ |
| static outputFixes(report) { |
| report.results.filter(result => result.hasOwnProperty("output")).forEach(result => { |
| fs.writeFileSync(result.filePath, result.output); |
| }); |
| } |
| |
| |
| /** |
| * Add a plugin by passing its configuration |
| * @param {string} name Name of the plugin. |
| * @param {Object} pluginobject Plugin configuration object. |
| * @returns {void} |
| */ |
| addPlugin(name, pluginobject) { |
| this.config.plugins.define(name, pluginobject); |
| } |
| |
| /** |
| * 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) { |
| return globUtil.resolveFileGlobPatterns(patterns, this.options); |
| } |
| |
| /** |
| * Executes the current configuration on an array of file and directory names. |
| * @param {string[]} patterns An array of file and directory names. |
| * @returns {Object} The results for all files that were linted. |
| */ |
| executeOnFiles(patterns) { |
| const options = this.options, |
| fileCache = this._fileCache, |
| configHelper = this.config; |
| const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd); |
| |
| if (!options.cache && fs.existsSync(cacheFile)) { |
| fs.unlinkSync(cacheFile); |
| } |
| |
| /** |
| * Calculates the hash of the config file used to validate a given file |
| * @param {string} filename The path of the file to retrieve a config object for to calculate the hash |
| * @returns {string} the hash of the config |
| */ |
| function hashOfConfigFor(filename) { |
| const config = configHelper.getConfig(filename); |
| |
| if (!configHashCache.has(config)) { |
| configHashCache.set(config, hash(`${pkg.version}_${stringify(config)}`)); |
| } |
| |
| return configHashCache.get(config); |
| } |
| |
| const startTime = Date.now(); |
| const fileList = globUtil.listFilesToProcess(this.resolveFileGlobPatterns(patterns), options); |
| const results = fileList.map(fileInfo => { |
| if (fileInfo.ignored) { |
| return createIgnoreResult(fileInfo.filename, options.cwd); |
| } |
| |
| if (options.cache) { |
| |
| /* |
| * get the descriptor for this file |
| * with the metadata and the flag that determines if |
| * the file has changed |
| */ |
| const descriptor = fileCache.getFileDescriptor(fileInfo.filename); |
| const hashOfConfig = hashOfConfigFor(fileInfo.filename); |
| const changed = descriptor.changed || descriptor.meta.hashOfConfig !== hashOfConfig; |
| |
| if (!changed) { |
| debug(`Skipping file since hasn't changed: ${fileInfo.filename}`); |
| |
| /* |
| * Add the the cached results (always will be 0 error and |
| * 0 warnings). We should not cache results for files that |
| * failed, in order to guarantee that next execution will |
| * process those files as well. |
| */ |
| return descriptor.meta.results; |
| } |
| } |
| |
| debug(`Processing ${fileInfo.filename}`); |
| |
| return processFile(fileInfo.filename, configHelper, options, this.linter); |
| }); |
| |
| if (options.cache) { |
| results.forEach(result => { |
| if (result.messages.length) { |
| |
| /* |
| * if a file contains errors or warnings we don't want to |
| * store the file in the cache so we can guarantee that |
| * next execution will also operate on this file |
| */ |
| fileCache.removeEntry(result.filePath); |
| } else { |
| |
| /* |
| * since the file passed we store the result here |
| * TODO: it might not be necessary to store the results list in the cache, |
| * since it should always be 0 errors/warnings |
| */ |
| const descriptor = fileCache.getFileDescriptor(result.filePath); |
| |
| descriptor.meta.hashOfConfig = hashOfConfigFor(result.filePath); |
| descriptor.meta.results = result; |
| } |
| }); |
| |
| // persist the cache to disk |
| fileCache.reconcile(); |
| } |
| |
| const stats = calculateStatsPerRun(results); |
| |
| debug(`Linting complete in: ${Date.now() - startTime}ms`); |
| |
| return { |
| results, |
| errorCount: stats.errorCount, |
| warningCount: stats.warningCount, |
| fixableErrorCount: stats.fixableErrorCount, |
| fixableWarningCount: stats.fixableWarningCount |
| }; |
| } |
| |
| /** |
| * 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 {Object} The results for the linting. |
| */ |
| executeOnText(text, filename, warnIgnored) { |
| |
| const results = [], |
| options = this.options, |
| configHelper = this.config, |
| ignoredPaths = new IgnoredPaths(options); |
| |
| // resolve filename based on options.cwd (for reporting, ignoredPaths also resolves) |
| |
| const resolvedFilename = filename && !path.isAbsolute(filename) |
| ? path.resolve(options.cwd, filename) |
| : filename; |
| |
| if (resolvedFilename && ignoredPaths.contains(resolvedFilename)) { |
| if (warnIgnored) { |
| results.push(createIgnoreResult(resolvedFilename, options.cwd)); |
| } |
| } else { |
| results.push( |
| processText( |
| text, |
| configHelper, |
| resolvedFilename, |
| options.fix, |
| options.allowInlineConfig, |
| options.reportUnusedDisableDirectives, |
| this.linter |
| ) |
| ); |
| } |
| |
| const stats = calculateStatsPerRun(results); |
| |
| return { |
| results, |
| errorCount: stats.errorCount, |
| warningCount: stats.warningCount, |
| fixableErrorCount: stats.fixableErrorCount, |
| fixableWarningCount: stats.fixableWarningCount |
| }; |
| } |
| |
| /** |
| * 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 {Object} A configuration object for the file. |
| */ |
| getConfigForFile(filePath) { |
| const configHelper = this.config; |
| |
| return configHelper.getConfig(filePath); |
| } |
| |
| /** |
| * 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 resolvedPath = path.resolve(this.options.cwd, filePath); |
| const ignoredPaths = new IgnoredPaths(this.options); |
| |
| return ignoredPaths.contains(resolvedPath); |
| } |
| |
| /** |
| * 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(/\\/g, "/"); |
| |
| const cwd = this.options ? this.options.cwd : process.cwd(); |
| const namespace = naming.getNamespaceFromTerm(normalizedFormatName); |
| |
| let formatterPath; |
| |
| // if there's a slash, then it's a file |
| if (!namespace && normalizedFormatName.indexOf("/") > -1) { |
| formatterPath = path.resolve(cwd, normalizedFormatName); |
| } else { |
| try { |
| const npmFormat = naming.normalizePackageName(normalizedFormatName, "eslint-formatter"); |
| |
| formatterPath = resolver.resolve(npmFormat, `${cwd}/node_modules`); |
| } catch (e) { |
| formatterPath = `./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; |