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