Spaces:
Runtime error
Runtime error
/** | |
* @fileoverview Main CLI object. | |
* @author Nicholas C. Zakas | |
*/ | |
; | |
/* | |
* 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 */ | |
//------------------------------------------------------------------------------ | |
// 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 {Object} cliOptions The CLI options to translate. | |
* @returns {ESLintOptions} The options object for the CLIEngine. | |
* @private | |
*/ | |
function translateOptions({ | |
cache, | |
cacheFile, | |
cacheLocation, | |
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, | |
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 warningCount = 0; | |
for (const result of results) { | |
errorCount += result.errorCount; | |
warningCount += result.warningCount; | |
} | |
return { errorCount, 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 = 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)); | |
} | |
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); | |
} | |
if (options.quiet) { | |
debug("Quiet mode enabled - filtering out warnings"); | |
results = ESLint.getErrorResults(results); | |
} | |
if (await printResults(engine, results, options.format, options.outputFile)) { | |
const { errorCount, warningCount } = countErrors(results); | |
const tooManyWarnings = | |
options.maxWarnings >= 0 && warningCount > options.maxWarnings; | |
if (!errorCount && tooManyWarnings) { | |
log.error( | |
"ESLint found too many warnings (maximum: %s).", | |
options.maxWarnings | |
); | |
} | |
return (errorCount || tooManyWarnings) ? 1 : 0; | |
} | |
return 2; | |
} | |
}; | |
module.exports = cli; | |