You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
344 lines
10 KiB
344 lines
10 KiB
/** |
|
* @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"), |
|
{ promisify } = require("util"), |
|
{ ESLint } = require("./eslint"), |
|
CLIOptions = require("./options"), |
|
log = require("./shared/logging"), |
|
RuntimeInfo = require("./shared/runtime-info"); |
|
|
|
const debug = require("debug")("eslint:cli"); |
|
|
|
//------------------------------------------------------------------------------ |
|
// Types |
|
//------------------------------------------------------------------------------ |
|
|
|
/** @typedef {import("./eslint/eslint").ESLintOptions} ESLintOptions */ |
|
/** @typedef {import("./eslint/eslint").LintMessage} LintMessage */ |
|
/** @typedef {import("./eslint/eslint").LintResult} LintResult */ |
|
/** @typedef {import("./options").ParsedCLIOptions} ParsedCLIOptions */ |
|
|
|
//------------------------------------------------------------------------------ |
|
// Helpers |
|
//------------------------------------------------------------------------------ |
|
|
|
const mkdir = promisify(fs.mkdir); |
|
const stat = promisify(fs.stat); |
|
const writeFile = promisify(fs.writeFile); |
|
|
|
/** |
|
* Predicate function for whether or not to apply fixes in quiet mode. |
|
* If a message is a warning, do not apply a fix. |
|
* @param {LintMessage} message The lint result. |
|
* @returns {boolean} True if the lint message is an error (and thus should be |
|
* autofixed), false otherwise. |
|
*/ |
|
function quietFixPredicate(message) { |
|
return message.severity === 2; |
|
} |
|
|
|
/** |
|
* Translates the CLI options into the options expected by the CLIEngine. |
|
* @param {ParsedCLIOptions} cliOptions The CLI options to translate. |
|
* @returns {ESLintOptions} The options object for the CLIEngine. |
|
* @private |
|
*/ |
|
function translateOptions({ |
|
cache, |
|
cacheFile, |
|
cacheLocation, |
|
cacheStrategy, |
|
config, |
|
env, |
|
errorOnUnmatchedPattern, |
|
eslintrc, |
|
ext, |
|
fix, |
|
fixDryRun, |
|
fixType, |
|
global, |
|
ignore, |
|
ignorePath, |
|
ignorePattern, |
|
inlineConfig, |
|
parser, |
|
parserOptions, |
|
plugin, |
|
quiet, |
|
reportUnusedDisableDirectives, |
|
resolvePluginsRelativeTo, |
|
rule, |
|
rulesdir |
|
}) { |
|
return { |
|
allowInlineConfig: inlineConfig, |
|
cache, |
|
cacheLocation: cacheLocation || cacheFile, |
|
cacheStrategy, |
|
errorOnUnmatchedPattern, |
|
extensions: ext, |
|
fix: (fix || fixDryRun) && (quiet ? quietFixPredicate : true), |
|
fixTypes: fixType, |
|
ignore, |
|
ignorePath, |
|
overrideConfig: { |
|
env: env && env.reduce((obj, name) => { |
|
obj[name] = true; |
|
return obj; |
|
}, {}), |
|
globals: global && global.reduce((obj, name) => { |
|
if (name.endsWith(":true")) { |
|
obj[name.slice(0, -5)] = "writable"; |
|
} else { |
|
obj[name] = "readonly"; |
|
} |
|
return obj; |
|
}, {}), |
|
ignorePatterns: ignorePattern, |
|
parser, |
|
parserOptions, |
|
plugins: plugin, |
|
rules: rule |
|
}, |
|
overrideConfigFile: config, |
|
reportUnusedDisableDirectives: reportUnusedDisableDirectives ? "error" : void 0, |
|
resolvePluginsRelativeTo, |
|
rulePaths: rulesdir, |
|
useEslintrc: eslintrc |
|
}; |
|
} |
|
|
|
/** |
|
* Count error messages. |
|
* @param {LintResult[]} results The lint results. |
|
* @returns {{errorCount:number;warningCount:number}} The number of error messages. |
|
*/ |
|
function countErrors(results) { |
|
let errorCount = 0; |
|
let fatalErrorCount = 0; |
|
let warningCount = 0; |
|
|
|
for (const result of results) { |
|
errorCount += result.errorCount; |
|
fatalErrorCount += result.fatalErrorCount; |
|
warningCount += result.warningCount; |
|
} |
|
|
|
return { errorCount, fatalErrorCount, warningCount }; |
|
} |
|
|
|
/** |
|
* Check if a given file path is a directory or not. |
|
* @param {string} filePath The path to a file to check. |
|
* @returns {Promise<boolean>} `true` if the given path is a directory. |
|
*/ |
|
async function isDirectory(filePath) { |
|
try { |
|
return (await stat(filePath)).isDirectory(); |
|
} catch (error) { |
|
if (error.code === "ENOENT" || error.code === "ENOTDIR") { |
|
return false; |
|
} |
|
throw error; |
|
} |
|
} |
|
|
|
/** |
|
* Outputs the results of the linting. |
|
* @param {ESLint} engine The ESLint instance to use. |
|
* @param {LintResult[]} results The results to print. |
|
* @param {string} format The name of the formatter to use or the path to the formatter. |
|
* @param {string} outputFile The path for the output file. |
|
* @returns {Promise<boolean>} True if the printing succeeds, false if not. |
|
* @private |
|
*/ |
|
async function printResults(engine, results, format, outputFile) { |
|
let formatter; |
|
|
|
try { |
|
formatter = await engine.loadFormatter(format); |
|
} catch (e) { |
|
log.error(e.message); |
|
return false; |
|
} |
|
|
|
const output = await formatter.format(results); |
|
|
|
if (output) { |
|
if (outputFile) { |
|
const filePath = path.resolve(process.cwd(), outputFile); |
|
|
|
if (await isDirectory(filePath)) { |
|
log.error("Cannot write to output file path, it is a directory: %s", outputFile); |
|
return false; |
|
} |
|
|
|
try { |
|
await mkdir(path.dirname(filePath), { recursive: true }); |
|
await writeFile(filePath, output); |
|
} catch (ex) { |
|
log.error("There was a problem writing the output file:\n%s", ex); |
|
return false; |
|
} |
|
} else { |
|
log.info(output); |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Public Interface |
|
//------------------------------------------------------------------------------ |
|
|
|
/** |
|
* Encapsulates all CLI behavior for eslint. Makes it easier to test as well as |
|
* for other Node.js programs to effectively run the CLI. |
|
*/ |
|
const cli = { |
|
|
|
/** |
|
* Executes the CLI based on an array of arguments that is passed in. |
|
* @param {string|Array|Object} args The arguments to process. |
|
* @param {string} [text] The text to lint (used for TTY). |
|
* @returns {Promise<number>} The exit code for the operation. |
|
*/ |
|
async execute(args, text) { |
|
if (Array.isArray(args)) { |
|
debug("CLI args: %o", args.slice(2)); |
|
} |
|
|
|
/** @type {ParsedCLIOptions} */ |
|
let options; |
|
|
|
try { |
|
options = CLIOptions.parse(args); |
|
} catch (error) { |
|
log.error(error.message); |
|
return 2; |
|
} |
|
|
|
const files = options._; |
|
const useStdin = typeof text === "string"; |
|
|
|
if (options.help) { |
|
log.info(CLIOptions.generateHelp()); |
|
return 0; |
|
} |
|
if (options.version) { |
|
log.info(RuntimeInfo.version()); |
|
return 0; |
|
} |
|
if (options.envInfo) { |
|
try { |
|
log.info(RuntimeInfo.environment()); |
|
return 0; |
|
} catch (err) { |
|
log.error(err.message); |
|
return 2; |
|
} |
|
} |
|
|
|
if (options.printConfig) { |
|
if (files.length) { |
|
log.error("The --print-config option must be used with exactly one file name."); |
|
return 2; |
|
} |
|
if (useStdin) { |
|
log.error("The --print-config option is not available for piped-in code."); |
|
return 2; |
|
} |
|
|
|
const engine = new ESLint(translateOptions(options)); |
|
const fileConfig = |
|
await engine.calculateConfigForFile(options.printConfig); |
|
|
|
log.info(JSON.stringify(fileConfig, null, " ")); |
|
return 0; |
|
} |
|
|
|
debug(`Running on ${useStdin ? "text" : "files"}`); |
|
|
|
if (options.fix && options.fixDryRun) { |
|
log.error("The --fix option and the --fix-dry-run option cannot be used together."); |
|
return 2; |
|
} |
|
if (useStdin && options.fix) { |
|
log.error("The --fix option is not available for piped-in code; use --fix-dry-run instead."); |
|
return 2; |
|
} |
|
if (options.fixType && !options.fix && !options.fixDryRun) { |
|
log.error("The --fix-type option requires either --fix or --fix-dry-run."); |
|
return 2; |
|
} |
|
|
|
const engine = new ESLint(translateOptions(options)); |
|
let results; |
|
|
|
if (useStdin) { |
|
results = await engine.lintText(text, { |
|
filePath: options.stdinFilename, |
|
warnIgnored: true |
|
}); |
|
} else { |
|
results = await engine.lintFiles(files); |
|
} |
|
|
|
if (options.fix) { |
|
debug("Fix mode enabled - applying fixes"); |
|
await ESLint.outputFixes(results); |
|
} |
|
|
|
let resultsToPrint = results; |
|
|
|
if (options.quiet) { |
|
debug("Quiet mode enabled - filtering out warnings"); |
|
resultsToPrint = ESLint.getErrorResults(resultsToPrint); |
|
} |
|
|
|
if (await printResults(engine, resultsToPrint, options.format, options.outputFile)) { |
|
|
|
// Errors and warnings from the original unfiltered results should determine the exit code |
|
const { errorCount, fatalErrorCount, warningCount } = countErrors(results); |
|
|
|
const tooManyWarnings = |
|
options.maxWarnings >= 0 && warningCount > options.maxWarnings; |
|
const shouldExitForFatalErrors = |
|
options.exitOnFatalError && fatalErrorCount > 0; |
|
|
|
if (!errorCount && tooManyWarnings) { |
|
log.error( |
|
"ESLint found too many warnings (maximum: %s).", |
|
options.maxWarnings |
|
); |
|
} |
|
|
|
if (shouldExitForFatalErrors) { |
|
return 2; |
|
} |
|
|
|
return (errorCount || tooManyWarnings) ? 1 : 0; |
|
} |
|
|
|
return 2; |
|
} |
|
}; |
|
|
|
module.exports = cli;
|
|
|