| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720 | /** * @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;
 |