• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview Main CLI object.
3 * @author Nicholas C. Zakas
4 */
5
6"use strict";
7
8/*
9 * The CLI object should *not* call process.exit() directly. It should only return
10 * exit codes. This allows other programs to use the CLI object and still control
11 * when the program exits.
12 */
13
14//------------------------------------------------------------------------------
15// Requirements
16//------------------------------------------------------------------------------
17
18const fs = require("fs"),
19    path = require("path"),
20    { promisify } = require("util"),
21    { ESLint } = require("./eslint"),
22    CLIOptions = require("./options"),
23    log = require("./shared/logging"),
24    RuntimeInfo = require("./shared/runtime-info");
25
26const debug = require("debug")("eslint:cli");
27
28//------------------------------------------------------------------------------
29// Types
30//------------------------------------------------------------------------------
31
32/** @typedef {import("./eslint/eslint").ESLintOptions} ESLintOptions */
33/** @typedef {import("./eslint/eslint").LintMessage} LintMessage */
34/** @typedef {import("./eslint/eslint").LintResult} LintResult */
35
36//------------------------------------------------------------------------------
37// Helpers
38//------------------------------------------------------------------------------
39
40const mkdir = promisify(fs.mkdir);
41const stat = promisify(fs.stat);
42const writeFile = promisify(fs.writeFile);
43
44/**
45 * Predicate function for whether or not to apply fixes in quiet mode.
46 * If a message is a warning, do not apply a fix.
47 * @param {LintMessage} message The lint result.
48 * @returns {boolean} True if the lint message is an error (and thus should be
49 * autofixed), false otherwise.
50 */
51function quietFixPredicate(message) {
52    return message.severity === 2;
53}
54
55/**
56 * Translates the CLI options into the options expected by the CLIEngine.
57 * @param {Object} cliOptions The CLI options to translate.
58 * @returns {ESLintOptions} The options object for the CLIEngine.
59 * @private
60 */
61function translateOptions({
62    cache,
63    cacheFile,
64    cacheLocation,
65    config,
66    env,
67    errorOnUnmatchedPattern,
68    eslintrc,
69    ext,
70    fix,
71    fixDryRun,
72    fixType,
73    global,
74    ignore,
75    ignorePath,
76    ignorePattern,
77    inlineConfig,
78    parser,
79    parserOptions,
80    plugin,
81    quiet,
82    reportUnusedDisableDirectives,
83    resolvePluginsRelativeTo,
84    rule,
85    rulesdir
86}) {
87    return {
88        allowInlineConfig: inlineConfig,
89        cache,
90        cacheLocation: cacheLocation || cacheFile,
91        errorOnUnmatchedPattern,
92        extensions: ext,
93        fix: (fix || fixDryRun) && (quiet ? quietFixPredicate : true),
94        fixTypes: fixType,
95        ignore,
96        ignorePath,
97        overrideConfig: {
98            env: env && env.reduce((obj, name) => {
99                obj[name] = true;
100                return obj;
101            }, {}),
102            globals: global && global.reduce((obj, name) => {
103                if (name.endsWith(":true")) {
104                    obj[name.slice(0, -5)] = "writable";
105                } else {
106                    obj[name] = "readonly";
107                }
108                return obj;
109            }, {}),
110            ignorePatterns: ignorePattern,
111            parser,
112            parserOptions,
113            plugins: plugin,
114            rules: rule
115        },
116        overrideConfigFile: config,
117        reportUnusedDisableDirectives: reportUnusedDisableDirectives ? "error" : void 0,
118        resolvePluginsRelativeTo,
119        rulePaths: rulesdir,
120        useEslintrc: eslintrc
121    };
122}
123
124/**
125 * Count error messages.
126 * @param {LintResult[]} results The lint results.
127 * @returns {{errorCount:number;warningCount:number}} The number of error messages.
128 */
129function countErrors(results) {
130    let errorCount = 0;
131    let warningCount = 0;
132
133    for (const result of results) {
134        errorCount += result.errorCount;
135        warningCount += result.warningCount;
136    }
137
138    return { errorCount, warningCount };
139}
140
141/**
142 * Check if a given file path is a directory or not.
143 * @param {string} filePath The path to a file to check.
144 * @returns {Promise<boolean>} `true` if the given path is a directory.
145 */
146async function isDirectory(filePath) {
147    try {
148        return (await stat(filePath)).isDirectory();
149    } catch (error) {
150        if (error.code === "ENOENT" || error.code === "ENOTDIR") {
151            return false;
152        }
153        throw error;
154    }
155}
156
157/**
158 * Outputs the results of the linting.
159 * @param {ESLint} engine The ESLint instance to use.
160 * @param {LintResult[]} results The results to print.
161 * @param {string} format The name of the formatter to use or the path to the formatter.
162 * @param {string} outputFile The path for the output file.
163 * @returns {Promise<boolean>} True if the printing succeeds, false if not.
164 * @private
165 */
166async function printResults(engine, results, format, outputFile) {
167    let formatter;
168
169    try {
170        formatter = await engine.loadFormatter(format);
171    } catch (e) {
172        log.error(e.message);
173        return false;
174    }
175
176    const output = formatter.format(results);
177
178    if (output) {
179        if (outputFile) {
180            const filePath = path.resolve(process.cwd(), outputFile);
181
182            if (await isDirectory(filePath)) {
183                log.error("Cannot write to output file path, it is a directory: %s", outputFile);
184                return false;
185            }
186
187            try {
188                await mkdir(path.dirname(filePath), { recursive: true });
189                await writeFile(filePath, output);
190            } catch (ex) {
191                log.error("There was a problem writing the output file:\n%s", ex);
192                return false;
193            }
194        } else {
195            log.info(output);
196        }
197    }
198
199    return true;
200}
201
202//------------------------------------------------------------------------------
203// Public Interface
204//------------------------------------------------------------------------------
205
206/**
207 * Encapsulates all CLI behavior for eslint. Makes it easier to test as well as
208 * for other Node.js programs to effectively run the CLI.
209 */
210const cli = {
211
212    /**
213     * Executes the CLI based on an array of arguments that is passed in.
214     * @param {string|Array|Object} args The arguments to process.
215     * @param {string} [text] The text to lint (used for TTY).
216     * @returns {Promise<number>} The exit code for the operation.
217     */
218    async execute(args, text) {
219        if (Array.isArray(args)) {
220            debug("CLI args: %o", args.slice(2));
221        }
222        let options;
223
224        try {
225            options = CLIOptions.parse(args);
226        } catch (error) {
227            log.error(error.message);
228            return 2;
229        }
230
231        const files = options._;
232        const useStdin = typeof text === "string";
233
234        if (options.help) {
235            log.info(CLIOptions.generateHelp());
236            return 0;
237        }
238        if (options.version) {
239            log.info(RuntimeInfo.version());
240            return 0;
241        }
242        if (options.envInfo) {
243            try {
244                log.info(RuntimeInfo.environment());
245                return 0;
246            } catch (err) {
247                log.error(err.message);
248                return 2;
249            }
250        }
251
252        if (options.printConfig) {
253            if (files.length) {
254                log.error("The --print-config option must be used with exactly one file name.");
255                return 2;
256            }
257            if (useStdin) {
258                log.error("The --print-config option is not available for piped-in code.");
259                return 2;
260            }
261
262            const engine = new ESLint(translateOptions(options));
263            const fileConfig =
264                await engine.calculateConfigForFile(options.printConfig);
265
266            log.info(JSON.stringify(fileConfig, null, "  "));
267            return 0;
268        }
269
270        debug(`Running on ${useStdin ? "text" : "files"}`);
271
272        if (options.fix && options.fixDryRun) {
273            log.error("The --fix option and the --fix-dry-run option cannot be used together.");
274            return 2;
275        }
276        if (useStdin && options.fix) {
277            log.error("The --fix option is not available for piped-in code; use --fix-dry-run instead.");
278            return 2;
279        }
280        if (options.fixType && !options.fix && !options.fixDryRun) {
281            log.error("The --fix-type option requires either --fix or --fix-dry-run.");
282            return 2;
283        }
284
285        const engine = new ESLint(translateOptions(options));
286        let results;
287
288        if (useStdin) {
289            results = await engine.lintText(text, {
290                filePath: options.stdinFilename,
291                warnIgnored: true
292            });
293        } else {
294            results = await engine.lintFiles(files);
295        }
296
297        if (options.fix) {
298            debug("Fix mode enabled - applying fixes");
299            await ESLint.outputFixes(results);
300        }
301
302        if (options.quiet) {
303            debug("Quiet mode enabled - filtering out warnings");
304            results = ESLint.getErrorResults(results);
305        }
306
307        if (await printResults(engine, results, options.format, options.outputFile)) {
308            const { errorCount, warningCount } = countErrors(results);
309            const tooManyWarnings =
310                options.maxWarnings >= 0 && warningCount > options.maxWarnings;
311
312            if (!errorCount && tooManyWarnings) {
313                log.error(
314                    "ESLint found too many warnings (maximum: %s).",
315                    options.maxWarnings
316                );
317            }
318
319            return (errorCount || tooManyWarnings) ? 1 : 0;
320        }
321
322        return 2;
323    }
324};
325
326module.exports = cli;
327