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