2024-08-09 12:04:48 +00:00
/ * *
* @ fileoverview Main CLI object .
* @ author Nicholas C . Zakas
* /
"use strict" ;
/ *
2024-08-21 06:34:30 +00:00
* NOTE : The CLI object should * not * call process . exit ( ) directly . It should only return
2024-08-09 12:04:48 +00:00
* 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" ) ,
2024-08-21 06:34:30 +00:00
{ FlatESLint , shouldUseFlatConfig } = require ( "./eslint/flat-eslint" ) ,
createCLIOptions = require ( "./options" ) ,
2024-08-09 12:04:48 +00:00
log = require ( "./shared/logging" ) ,
2024-08-21 06:34:30 +00:00
RuntimeInfo = require ( "./shared/runtime-info" ) ,
{ normalizeSeverityToString } = require ( "./shared/severity" ) ;
const { Legacy : { naming } } = require ( "@eslint/eslintrc" ) ;
const { ModuleImporter } = require ( "@humanwhocodes/module-importer" ) ;
2024-08-09 12:04:48 +00:00
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 */
2024-08-21 06:34:30 +00:00
/** @typedef {import("./shared/types").ResultsMeta} ResultsMeta */
2024-08-09 12:04:48 +00:00
//------------------------------------------------------------------------------
// 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 ;
}
/ * *
2024-08-21 06:34:30 +00:00
* Translates the CLI options into the options expected by the ESLint constructor .
2024-08-09 12:04:48 +00:00
* @ param { ParsedCLIOptions } cliOptions The CLI options to translate .
2024-08-21 06:34:30 +00:00
* @ param { "flat" | "eslintrc" } [ configType = "eslintrc" ] The format of the
* config to generate .
* @ returns { Promise < ESLintOptions > } The options object for the ESLint constructor .
2024-08-09 12:04:48 +00:00
* @ private
* /
2024-08-21 06:34:30 +00:00
async function translateOptions ( {
2024-08-09 12:04:48 +00:00
cache ,
cacheFile ,
cacheLocation ,
cacheStrategy ,
config ,
2024-08-21 06:34:30 +00:00
configLookup ,
2024-08-09 12:04:48 +00:00
env ,
errorOnUnmatchedPattern ,
eslintrc ,
ext ,
fix ,
fixDryRun ,
fixType ,
global ,
ignore ,
ignorePath ,
ignorePattern ,
inlineConfig ,
parser ,
parserOptions ,
plugin ,
quiet ,
reportUnusedDisableDirectives ,
2024-08-21 06:34:30 +00:00
reportUnusedDisableDirectivesSeverity ,
2024-08-09 12:04:48 +00:00
resolvePluginsRelativeTo ,
rule ,
2024-08-21 06:34:30 +00:00
rulesdir ,
warnIgnored
} , configType ) {
let overrideConfig , overrideConfigFile ;
const importer = new ModuleImporter ( ) ;
if ( configType === "flat" ) {
overrideConfigFile = ( typeof config === "string" ) ? config : ! configLookup ;
if ( overrideConfigFile === false ) {
overrideConfigFile = void 0 ;
}
let globals = { } ;
if ( global ) {
globals = global . reduce ( ( obj , name ) => {
if ( name . endsWith ( ":true" ) ) {
obj [ name . slice ( 0 , - 5 ) ] = "writable" ;
} else {
obj [ name ] = "readonly" ;
}
return obj ;
} , globals ) ;
}
overrideConfig = [ {
languageOptions : {
globals ,
parserOptions : parserOptions || { }
} ,
rules : rule ? rule : { }
} ] ;
if ( reportUnusedDisableDirectives || reportUnusedDisableDirectivesSeverity !== void 0 ) {
overrideConfig [ 0 ] . linterOptions = {
reportUnusedDisableDirectives : reportUnusedDisableDirectives
? "error"
: normalizeSeverityToString ( reportUnusedDisableDirectivesSeverity )
} ;
}
if ( parser ) {
overrideConfig [ 0 ] . languageOptions . parser = await importer . import ( parser ) ;
}
if ( plugin ) {
const plugins = { } ;
for ( const pluginName of plugin ) {
const shortName = naming . getShorthandName ( pluginName , "eslint-plugin" ) ;
const longName = naming . normalizePackageName ( pluginName , "eslint-plugin" ) ;
plugins [ shortName ] = await importer . import ( longName ) ;
}
overrideConfig [ 0 ] . plugins = plugins ;
}
} else {
overrideConfigFile = config ;
overrideConfig = {
2024-08-09 12:04:48 +00:00
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
2024-08-21 06:34:30 +00:00
} ;
}
const options = {
allowInlineConfig : inlineConfig ,
cache ,
cacheLocation : cacheLocation || cacheFile ,
cacheStrategy ,
errorOnUnmatchedPattern ,
fix : ( fix || fixDryRun ) && ( quiet ? quietFixPredicate : true ) ,
fixTypes : fixType ,
ignore ,
overrideConfig ,
overrideConfigFile
2024-08-09 12:04:48 +00:00
} ;
2024-08-21 06:34:30 +00:00
if ( configType === "flat" ) {
options . ignorePatterns = ignorePattern ;
options . warnIgnored = warnIgnored ;
} else {
options . resolvePluginsRelativeTo = resolvePluginsRelativeTo ;
options . rulePaths = rulesdir ;
options . useEslintrc = eslintrc ;
options . extensions = ext ;
options . ignorePath = ignorePath ;
if ( reportUnusedDisableDirectives || reportUnusedDisableDirectivesSeverity !== void 0 ) {
options . reportUnusedDisableDirectives = reportUnusedDisableDirectives
? "error"
: normalizeSeverityToString ( reportUnusedDisableDirectivesSeverity ) ;
}
}
return options ;
2024-08-09 12:04:48 +00:00
}
/ * *
* Count error messages .
* @ param { LintResult [ ] } results The lint results .
2024-08-21 06:34:30 +00:00
* @ returns { { errorCount : number ; fatalErrorCount : number , warningCount : number } } The number of error messages .
2024-08-09 12:04:48 +00:00
* /
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 .
2024-08-21 06:34:30 +00:00
* @ param { ResultsMeta } resultsMeta Warning count and max threshold .
2024-08-09 12:04:48 +00:00
* @ returns { Promise < boolean > } True if the printing succeeds , false if not .
* @ private
* /
2024-08-21 06:34:30 +00:00
async function printResults ( engine , results , format , outputFile , resultsMeta ) {
2024-08-09 12:04:48 +00:00
let formatter ;
try {
formatter = await engine . loadFormatter ( format ) ;
} catch ( e ) {
log . error ( e . message ) ;
return false ;
}
2024-08-21 06:34:30 +00:00
const output = await formatter . format ( results , resultsMeta ) ;
2024-08-09 12:04:48 +00:00
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 ) .
2024-08-21 06:34:30 +00:00
* @ param { boolean } [ allowFlatConfig ] Whether or not to allow flat config .
2024-08-09 12:04:48 +00:00
* @ returns { Promise < number > } The exit code for the operation .
* /
2024-08-21 06:34:30 +00:00
async execute ( args , text , allowFlatConfig ) {
2024-08-09 12:04:48 +00:00
if ( Array . isArray ( args ) ) {
debug ( "CLI args: %o" , args . slice ( 2 ) ) ;
}
2024-08-21 06:34:30 +00:00
/ *
* Before doing anything , we need to see if we are using a
* flat config file . If so , then we need to change the way command
* line args are parsed . This is temporary , and when we fully
* switch to flat config we can remove this logic .
* /
const usingFlatConfig = allowFlatConfig && await shouldUseFlatConfig ( ) ;
debug ( "Using flat config?" , usingFlatConfig ) ;
const CLIOptions = createCLIOptions ( usingFlatConfig ) ;
2024-08-09 12:04:48 +00:00
/** @type {ParsedCLIOptions} */
let options ;
try {
options = CLIOptions . parse ( args ) ;
} catch ( error ) {
2024-08-21 06:34:30 +00:00
debug ( "Error parsing CLI options:" , error . message ) ;
let errorMessage = error . message ;
if ( usingFlatConfig ) {
errorMessage += "\nYou're using eslint.config.js, some command line flags are no longer available. Please see https://eslint.org/docs/latest/use/command-line-interface for details." ;
}
log . error ( errorMessage ) ;
2024-08-09 12:04:48 +00:00
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 ) {
2024-08-21 06:34:30 +00:00
debug ( "Error retrieving environment info" ) ;
2024-08-09 12:04:48 +00:00
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 ;
}
2024-08-21 06:34:30 +00:00
const engine = usingFlatConfig
? new FlatESLint ( await translateOptions ( options , "flat" ) )
: new ESLint ( await translateOptions ( options ) ) ;
2024-08-09 12:04:48 +00:00
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 ;
}
2024-08-21 06:34:30 +00:00
if ( options . reportUnusedDisableDirectives && options . reportUnusedDisableDirectivesSeverity !== void 0 ) {
log . error ( "The --report-unused-disable-directives option and the --report-unused-disable-directives-severity option cannot be used together." ) ;
return 2 ;
}
const ActiveESLint = usingFlatConfig ? FlatESLint : ESLint ;
const engine = new ActiveESLint ( await translateOptions ( options , usingFlatConfig ? "flat" : "eslintrc" ) ) ;
2024-08-09 12:04:48 +00:00
let results ;
if ( useStdin ) {
results = await engine . lintText ( text , {
filePath : options . stdinFilename ,
2024-08-21 06:34:30 +00:00
// flatConfig respects CLI flag and constructor warnIgnored, eslintrc forces true for backwards compatibility
warnIgnored : usingFlatConfig ? void 0 : true
2024-08-09 12:04:48 +00:00
} ) ;
} else {
results = await engine . lintFiles ( files ) ;
}
if ( options . fix ) {
debug ( "Fix mode enabled - applying fixes" ) ;
2024-08-21 06:34:30 +00:00
await ActiveESLint . outputFixes ( results ) ;
2024-08-09 12:04:48 +00:00
}
let resultsToPrint = results ;
if ( options . quiet ) {
debug ( "Quiet mode enabled - filtering out warnings" ) ;
2024-08-21 06:34:30 +00:00
resultsToPrint = ActiveESLint . getErrorResults ( resultsToPrint ) ;
2024-08-09 12:04:48 +00:00
}
2024-08-21 06:34:30 +00:00
const resultCounts = countErrors ( results ) ;
const tooManyWarnings = options . maxWarnings >= 0 && resultCounts . warningCount > options . maxWarnings ;
const resultsMeta = tooManyWarnings
? {
maxWarningsExceeded : {
maxWarnings : options . maxWarnings ,
foundWarnings : resultCounts . warningCount
}
}
: { } ;
2024-08-09 12:04:48 +00:00
2024-08-21 06:34:30 +00:00
if ( await printResults ( engine , resultsToPrint , options . format , options . outputFile , resultsMeta ) ) {
2024-08-09 12:04:48 +00:00
2024-08-21 06:34:30 +00:00
// Errors and warnings from the original unfiltered results should determine the exit code
2024-08-09 12:04:48 +00:00
const shouldExitForFatalErrors =
2024-08-21 06:34:30 +00:00
options . exitOnFatalError && resultCounts . fatalErrorCount > 0 ;
2024-08-09 12:04:48 +00:00
2024-08-21 06:34:30 +00:00
if ( ! resultCounts . errorCount && tooManyWarnings ) {
2024-08-09 12:04:48 +00:00
log . error (
"ESLint found too many warnings (maximum: %s)." ,
options . maxWarnings
) ;
}
if ( shouldExitForFatalErrors ) {
return 2 ;
}
2024-08-21 06:34:30 +00:00
return ( resultCounts . errorCount || tooManyWarnings ) ? 1 : 0 ;
2024-08-09 12:04:48 +00:00
}
return 2 ;
}
} ;
module . exports = cli ;