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"); 19const path = require("path"); 20const defaultOptions = require("../../conf/default-cli-options"); 21const pkg = require("../../package.json"); 22const ConfigOps = require("@eslint/eslintrc/lib/shared/config-ops"); 23const naming = require("@eslint/eslintrc/lib/shared/naming"); 24const ModuleResolver = require("../shared/relative-module-resolver"); 25const { Linter } = require("../linter"); 26const builtInRules = require("../rules"); 27const { CascadingConfigArrayFactory } = require("./cascading-config-array-factory"); 28const { IgnorePattern, getUsedExtractedConfigs } = require("./config-array"); 29const { FileEnumerator } = require("./file-enumerator"); 30const hash = require("./hash"); 31const LintResultCache = require("./lint-result-cache"); 32 33const debug = require("debug")("eslint:cli-engine"); 34const validFixTypes = new Set(["problem", "suggestion", "layout"]); 35 36//------------------------------------------------------------------------------ 37// Typedefs 38//------------------------------------------------------------------------------ 39 40// For VSCode IntelliSense 41/** @typedef {import("../shared/types").ConfigData} ConfigData */ 42/** @typedef {import("../shared/types").DeprecatedRuleInfo} DeprecatedRuleInfo */ 43/** @typedef {import("../shared/types").LintMessage} LintMessage */ 44/** @typedef {import("../shared/types").ParserOptions} ParserOptions */ 45/** @typedef {import("../shared/types").Plugin} Plugin */ 46/** @typedef {import("../shared/types").RuleConf} RuleConf */ 47/** @typedef {import("../shared/types").Rule} Rule */ 48/** @typedef {ReturnType<CascadingConfigArrayFactory["getConfigArrayForFile"]>} ConfigArray */ 49/** @typedef {ReturnType<ConfigArray["extractConfig"]>} ExtractedConfig */ 50 51/** 52 * The options to configure a CLI engine with. 53 * @typedef {Object} CLIEngineOptions 54 * @property {boolean} [allowInlineConfig] Enable or disable inline configuration comments. 55 * @property {ConfigData} [baseConfig] Base config object, extended by all configs used with this CLIEngine instance 56 * @property {boolean} [cache] Enable result caching. 57 * @property {string} [cacheLocation] The cache file to use instead of .eslintcache. 58 * @property {string} [configFile] The configuration file to use. 59 * @property {string} [cwd] The value to use for the current working directory. 60 * @property {string[]} [envs] An array of environments to load. 61 * @property {string[]|null} [extensions] An array of file extensions to check. 62 * @property {boolean|Function} [fix] Execute in autofix mode. If a function, should return a boolean. 63 * @property {string[]} [fixTypes] Array of rule types to apply fixes for. 64 * @property {string[]} [globals] An array of global variables to declare. 65 * @property {boolean} [ignore] False disables use of .eslintignore. 66 * @property {string} [ignorePath] The ignore file to use instead of .eslintignore. 67 * @property {string|string[]} [ignorePattern] One or more glob patterns to ignore. 68 * @property {boolean} [useEslintrc] False disables looking for .eslintrc 69 * @property {string} [parser] The name of the parser to use. 70 * @property {ParserOptions} [parserOptions] An object of parserOption settings to use. 71 * @property {string[]} [plugins] An array of plugins to load. 72 * @property {Record<string,RuleConf>} [rules] An object of rules to use. 73 * @property {string[]} [rulePaths] An array of directories to load custom rules from. 74 * @property {boolean} [reportUnusedDisableDirectives] `true` adds reports for unused eslint-disable directives 75 * @property {boolean} [globInputPaths] Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file. 76 * @property {string} [resolvePluginsRelativeTo] The folder where plugins should be resolved from, defaulting to the CWD 77 */ 78 79/** 80 * A linting result. 81 * @typedef {Object} LintResult 82 * @property {string} filePath The path to the file that was linted. 83 * @property {LintMessage[]} messages All of the messages for the result. 84 * @property {number} errorCount Number of errors for the result. 85 * @property {number} warningCount Number of warnings for the result. 86 * @property {number} fixableErrorCount Number of fixable errors for the result. 87 * @property {number} fixableWarningCount Number of fixable warnings for the result. 88 * @property {string} [source] The source code of the file that was linted. 89 * @property {string} [output] The source code of the file that was linted, with as many fixes applied as possible. 90 */ 91 92/** 93 * Linting results. 94 * @typedef {Object} LintReport 95 * @property {LintResult[]} results All of the result. 96 * @property {number} errorCount Number of errors for the result. 97 * @property {number} warningCount Number of warnings for the result. 98 * @property {number} fixableErrorCount Number of fixable errors for the result. 99 * @property {number} fixableWarningCount Number of fixable warnings for the result. 100 * @property {DeprecatedRuleInfo[]} usedDeprecatedRules The list of used deprecated rules. 101 */ 102 103/** 104 * Private data for CLIEngine. 105 * @typedef {Object} CLIEngineInternalSlots 106 * @property {Map<string, Plugin>} additionalPluginPool The map for additional plugins. 107 * @property {string} cacheFilePath The path to the cache of lint results. 108 * @property {CascadingConfigArrayFactory} configArrayFactory The factory of configs. 109 * @property {(filePath: string) => boolean} defaultIgnores The default predicate function to check if a file ignored or not. 110 * @property {FileEnumerator} fileEnumerator The file enumerator. 111 * @property {ConfigArray[]} lastConfigArrays The list of config arrays that the last `executeOnFiles` or `executeOnText` used. 112 * @property {LintResultCache|null} lintResultCache The cache of lint results. 113 * @property {Linter} linter The linter instance which has loaded rules. 114 * @property {CLIEngineOptions} options The normalized options of this instance. 115 */ 116 117//------------------------------------------------------------------------------ 118// Helpers 119//------------------------------------------------------------------------------ 120 121/** @type {WeakMap<CLIEngine, CLIEngineInternalSlots>} */ 122const internalSlotsMap = new WeakMap(); 123 124/** 125 * Determines if each fix type in an array is supported by ESLint and throws 126 * an error if not. 127 * @param {string[]} fixTypes An array of fix types to check. 128 * @returns {void} 129 * @throws {Error} If an invalid fix type is found. 130 */ 131function validateFixTypes(fixTypes) { 132 for (const fixType of fixTypes) { 133 if (!validFixTypes.has(fixType)) { 134 throw new Error(`Invalid fix type "${fixType}" found.`); 135 } 136 } 137} 138 139/** 140 * It will calculate the error and warning count for collection of messages per file 141 * @param {LintMessage[]} messages Collection of messages 142 * @returns {Object} Contains the stats 143 * @private 144 */ 145function calculateStatsPerFile(messages) { 146 return messages.reduce((stat, message) => { 147 if (message.fatal || message.severity === 2) { 148 stat.errorCount++; 149 if (message.fix) { 150 stat.fixableErrorCount++; 151 } 152 } else { 153 stat.warningCount++; 154 if (message.fix) { 155 stat.fixableWarningCount++; 156 } 157 } 158 return stat; 159 }, { 160 errorCount: 0, 161 warningCount: 0, 162 fixableErrorCount: 0, 163 fixableWarningCount: 0 164 }); 165} 166 167/** 168 * It will calculate the error and warning count for collection of results from all files 169 * @param {LintResult[]} results Collection of messages from all the files 170 * @returns {Object} Contains the stats 171 * @private 172 */ 173function calculateStatsPerRun(results) { 174 return results.reduce((stat, result) => { 175 stat.errorCount += result.errorCount; 176 stat.warningCount += result.warningCount; 177 stat.fixableErrorCount += result.fixableErrorCount; 178 stat.fixableWarningCount += result.fixableWarningCount; 179 return stat; 180 }, { 181 errorCount: 0, 182 warningCount: 0, 183 fixableErrorCount: 0, 184 fixableWarningCount: 0 185 }); 186} 187 188/** 189 * Processes an source code using ESLint. 190 * @param {Object} config The config object. 191 * @param {string} config.text The source code to verify. 192 * @param {string} config.cwd The path to the current working directory. 193 * @param {string|undefined} config.filePath The path to the file of `text`. If this is undefined, it uses `<text>`. 194 * @param {ConfigArray} config.config The config. 195 * @param {boolean} config.fix If `true` then it does fix. 196 * @param {boolean} config.allowInlineConfig If `true` then it uses directive comments. 197 * @param {boolean} config.reportUnusedDisableDirectives If `true` then it reports unused `eslint-disable` comments. 198 * @param {FileEnumerator} config.fileEnumerator The file enumerator to check if a path is a target or not. 199 * @param {Linter} config.linter The linter instance to verify. 200 * @returns {LintResult} The result of linting. 201 * @private 202 */ 203function verifyText({ 204 text, 205 cwd, 206 filePath: providedFilePath, 207 config, 208 fix, 209 allowInlineConfig, 210 reportUnusedDisableDirectives, 211 fileEnumerator, 212 linter 213}) { 214 const filePath = providedFilePath || "<text>"; 215 216 debug(`Lint ${filePath}`); 217 218 /* 219 * Verify. 220 * `config.extractConfig(filePath)` requires an absolute path, but `linter` 221 * doesn't know CWD, so it gives `linter` an absolute path always. 222 */ 223 const filePathToVerify = filePath === "<text>" ? path.join(cwd, filePath) : filePath; 224 const { fixed, messages, output } = linter.verifyAndFix( 225 text, 226 config, 227 { 228 allowInlineConfig, 229 filename: filePathToVerify, 230 fix, 231 reportUnusedDisableDirectives, 232 233 /** 234 * Check if the linter should adopt a given code block or not. 235 * @param {string} blockFilename The virtual filename of a code block. 236 * @returns {boolean} `true` if the linter should adopt the code block. 237 */ 238 filterCodeBlock(blockFilename) { 239 return fileEnumerator.isTargetPath(blockFilename); 240 } 241 } 242 ); 243 244 // Tweak and return. 245 const result = { 246 filePath, 247 messages, 248 ...calculateStatsPerFile(messages) 249 }; 250 251 if (fixed) { 252 result.output = output; 253 } 254 if ( 255 result.errorCount + result.warningCount > 0 && 256 typeof result.output === "undefined" 257 ) { 258 result.source = text; 259 } 260 261 return result; 262} 263 264/** 265 * Returns result with warning by ignore settings 266 * @param {string} filePath File path of checked code 267 * @param {string} baseDir Absolute path of base directory 268 * @returns {LintResult} Result with single warning 269 * @private 270 */ 271function createIgnoreResult(filePath, baseDir) { 272 let message; 273 const isHidden = filePath.split(path.sep) 274 .find(segment => /^\./u.test(segment)); 275 const isInNodeModules = baseDir && path.relative(baseDir, filePath).startsWith("node_modules"); 276 277 if (isHidden) { 278 message = "File ignored by default. Use a negated ignore pattern (like \"--ignore-pattern '!<relative/path/to/filename>'\") to override."; 279 } else if (isInNodeModules) { 280 message = "File ignored by default. Use \"--ignore-pattern '!node_modules/*'\" to override."; 281 } else { 282 message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override."; 283 } 284 285 return { 286 filePath: path.resolve(filePath), 287 messages: [ 288 { 289 fatal: false, 290 severity: 1, 291 message 292 } 293 ], 294 errorCount: 0, 295 warningCount: 1, 296 fixableErrorCount: 0, 297 fixableWarningCount: 0 298 }; 299} 300 301/** 302 * Get a rule. 303 * @param {string} ruleId The rule ID to get. 304 * @param {ConfigArray[]} configArrays The config arrays that have plugin rules. 305 * @returns {Rule|null} The rule or null. 306 */ 307function getRule(ruleId, configArrays) { 308 for (const configArray of configArrays) { 309 const rule = configArray.pluginRules.get(ruleId); 310 311 if (rule) { 312 return rule; 313 } 314 } 315 return builtInRules.get(ruleId) || null; 316} 317 318/** 319 * Collect used deprecated rules. 320 * @param {ConfigArray[]} usedConfigArrays The config arrays which were used. 321 * @returns {IterableIterator<DeprecatedRuleInfo>} Used deprecated rules. 322 */ 323function *iterateRuleDeprecationWarnings(usedConfigArrays) { 324 const processedRuleIds = new Set(); 325 326 // Flatten used configs. 327 /** @type {ExtractedConfig[]} */ 328 const configs = [].concat( 329 ...usedConfigArrays.map(getUsedExtractedConfigs) 330 ); 331 332 // Traverse rule configs. 333 for (const config of configs) { 334 for (const [ruleId, ruleConfig] of Object.entries(config.rules)) { 335 336 // Skip if it was processed. 337 if (processedRuleIds.has(ruleId)) { 338 continue; 339 } 340 processedRuleIds.add(ruleId); 341 342 // Skip if it's not used. 343 if (!ConfigOps.getRuleSeverity(ruleConfig)) { 344 continue; 345 } 346 const rule = getRule(ruleId, usedConfigArrays); 347 348 // Skip if it's not deprecated. 349 if (!(rule && rule.meta && rule.meta.deprecated)) { 350 continue; 351 } 352 353 // This rule was used and deprecated. 354 yield { 355 ruleId, 356 replacedBy: rule.meta.replacedBy || [] 357 }; 358 } 359 } 360} 361 362/** 363 * Checks if the given message is an error message. 364 * @param {LintMessage} message The message to check. 365 * @returns {boolean} Whether or not the message is an error message. 366 * @private 367 */ 368function isErrorMessage(message) { 369 return message.severity === 2; 370} 371 372 373/** 374 * return the cacheFile to be used by eslint, based on whether the provided parameter is 375 * a directory or looks like a directory (ends in `path.sep`), in which case the file 376 * name will be the `cacheFile/.cache_hashOfCWD` 377 * 378 * if cacheFile points to a file or looks like a file then in will just use that file 379 * @param {string} cacheFile The name of file to be used to store the cache 380 * @param {string} cwd Current working directory 381 * @returns {string} the resolved path to the cache file 382 */ 383function getCacheFile(cacheFile, cwd) { 384 385 /* 386 * make sure the path separators are normalized for the environment/os 387 * keeping the trailing path separator if present 388 */ 389 const normalizedCacheFile = path.normalize(cacheFile); 390 391 const resolvedCacheFile = path.resolve(cwd, normalizedCacheFile); 392 const looksLikeADirectory = normalizedCacheFile.slice(-1) === path.sep; 393 394 /** 395 * return the name for the cache file in case the provided parameter is a directory 396 * @returns {string} the resolved path to the cacheFile 397 */ 398 function getCacheFileForDirectory() { 399 return path.join(resolvedCacheFile, `.cache_${hash(cwd)}`); 400 } 401 402 let fileStats; 403 404 try { 405 fileStats = fs.lstatSync(resolvedCacheFile); 406 } catch { 407 fileStats = null; 408 } 409 410 411 /* 412 * in case the file exists we need to verify if the provided path 413 * is a directory or a file. If it is a directory we want to create a file 414 * inside that directory 415 */ 416 if (fileStats) { 417 418 /* 419 * is a directory or is a file, but the original file the user provided 420 * looks like a directory but `path.resolve` removed the `last path.sep` 421 * so we need to still treat this like a directory 422 */ 423 if (fileStats.isDirectory() || looksLikeADirectory) { 424 return getCacheFileForDirectory(); 425 } 426 427 // is file so just use that file 428 return resolvedCacheFile; 429 } 430 431 /* 432 * here we known the file or directory doesn't exist, 433 * so we will try to infer if its a directory if it looks like a directory 434 * for the current operating system. 435 */ 436 437 // if the last character passed is a path separator we assume is a directory 438 if (looksLikeADirectory) { 439 return getCacheFileForDirectory(); 440 } 441 442 return resolvedCacheFile; 443} 444 445/** 446 * Convert a string array to a boolean map. 447 * @param {string[]|null} keys The keys to assign true. 448 * @param {boolean} defaultValue The default value for each property. 449 * @param {string} displayName The property name which is used in error message. 450 * @returns {Record<string,boolean>} The boolean map. 451 */ 452function toBooleanMap(keys, defaultValue, displayName) { 453 if (keys && !Array.isArray(keys)) { 454 throw new Error(`${displayName} must be an array.`); 455 } 456 if (keys && keys.length > 0) { 457 return keys.reduce((map, def) => { 458 const [key, value] = def.split(":"); 459 460 if (key !== "__proto__") { 461 map[key] = value === void 0 462 ? defaultValue 463 : value === "true"; 464 } 465 466 return map; 467 }, {}); 468 } 469 return void 0; 470} 471 472/** 473 * Create a config data from CLI options. 474 * @param {CLIEngineOptions} options The options 475 * @returns {ConfigData|null} The created config data. 476 */ 477function createConfigDataFromOptions(options) { 478 const { 479 ignorePattern, 480 parser, 481 parserOptions, 482 plugins, 483 rules 484 } = options; 485 const env = toBooleanMap(options.envs, true, "envs"); 486 const globals = toBooleanMap(options.globals, false, "globals"); 487 488 if ( 489 env === void 0 && 490 globals === void 0 && 491 (ignorePattern === void 0 || ignorePattern.length === 0) && 492 parser === void 0 && 493 parserOptions === void 0 && 494 plugins === void 0 && 495 rules === void 0 496 ) { 497 return null; 498 } 499 return { 500 env, 501 globals, 502 ignorePatterns: ignorePattern, 503 parser, 504 parserOptions, 505 plugins, 506 rules 507 }; 508} 509 510/** 511 * Checks whether a directory exists at the given location 512 * @param {string} resolvedPath A path from the CWD 513 * @returns {boolean} `true` if a directory exists 514 */ 515function directoryExists(resolvedPath) { 516 try { 517 return fs.statSync(resolvedPath).isDirectory(); 518 } catch (error) { 519 if (error && error.code === "ENOENT") { 520 return false; 521 } 522 throw error; 523 } 524} 525 526//------------------------------------------------------------------------------ 527// Public Interface 528//------------------------------------------------------------------------------ 529 530class CLIEngine { 531 532 /** 533 * Creates a new instance of the core CLI engine. 534 * @param {CLIEngineOptions} providedOptions The options for this instance. 535 */ 536 constructor(providedOptions) { 537 const options = Object.assign( 538 Object.create(null), 539 defaultOptions, 540 { cwd: process.cwd() }, 541 providedOptions 542 ); 543 544 if (options.fix === void 0) { 545 options.fix = false; 546 } 547 548 const additionalPluginPool = new Map(); 549 const cacheFilePath = getCacheFile( 550 options.cacheLocation || options.cacheFile, 551 options.cwd 552 ); 553 const configArrayFactory = new CascadingConfigArrayFactory({ 554 additionalPluginPool, 555 baseConfig: options.baseConfig || null, 556 cliConfig: createConfigDataFromOptions(options), 557 cwd: options.cwd, 558 ignorePath: options.ignorePath, 559 resolvePluginsRelativeTo: options.resolvePluginsRelativeTo, 560 rulePaths: options.rulePaths, 561 specificConfigPath: options.configFile, 562 useEslintrc: options.useEslintrc 563 }); 564 const fileEnumerator = new FileEnumerator({ 565 configArrayFactory, 566 cwd: options.cwd, 567 extensions: options.extensions, 568 globInputPaths: options.globInputPaths, 569 errorOnUnmatchedPattern: options.errorOnUnmatchedPattern, 570 ignore: options.ignore 571 }); 572 const lintResultCache = 573 options.cache ? new LintResultCache(cacheFilePath) : null; 574 const linter = new Linter({ cwd: options.cwd }); 575 576 /** @type {ConfigArray[]} */ 577 const lastConfigArrays = [configArrayFactory.getConfigArrayForFile()]; 578 579 // Store private data. 580 internalSlotsMap.set(this, { 581 additionalPluginPool, 582 cacheFilePath, 583 configArrayFactory, 584 defaultIgnores: IgnorePattern.createDefaultIgnore(options.cwd), 585 fileEnumerator, 586 lastConfigArrays, 587 lintResultCache, 588 linter, 589 options 590 }); 591 592 // setup special filter for fixes 593 if (options.fix && options.fixTypes && options.fixTypes.length > 0) { 594 debug(`Using fix types ${options.fixTypes}`); 595 596 // throw an error if any invalid fix types are found 597 validateFixTypes(options.fixTypes); 598 599 // convert to Set for faster lookup 600 const fixTypes = new Set(options.fixTypes); 601 602 // save original value of options.fix in case it's a function 603 const originalFix = (typeof options.fix === "function") 604 ? options.fix : () => true; 605 606 options.fix = message => { 607 const rule = message.ruleId && getRule(message.ruleId, lastConfigArrays); 608 const matches = rule && rule.meta && fixTypes.has(rule.meta.type); 609 610 return matches && originalFix(message); 611 }; 612 } 613 } 614 615 getRules() { 616 const { lastConfigArrays } = internalSlotsMap.get(this); 617 618 return new Map(function *() { 619 yield* builtInRules; 620 621 for (const configArray of lastConfigArrays) { 622 yield* configArray.pluginRules; 623 } 624 }()); 625 } 626 627 /** 628 * Returns results that only contains errors. 629 * @param {LintResult[]} results The results to filter. 630 * @returns {LintResult[]} The filtered results. 631 */ 632 static getErrorResults(results) { 633 const filtered = []; 634 635 results.forEach(result => { 636 const filteredMessages = result.messages.filter(isErrorMessage); 637 638 if (filteredMessages.length > 0) { 639 filtered.push({ 640 ...result, 641 messages: filteredMessages, 642 errorCount: filteredMessages.length, 643 warningCount: 0, 644 fixableErrorCount: result.fixableErrorCount, 645 fixableWarningCount: 0 646 }); 647 } 648 }); 649 650 return filtered; 651 } 652 653 /** 654 * Outputs fixes from the given results to files. 655 * @param {LintReport} report The report object created by CLIEngine. 656 * @returns {void} 657 */ 658 static outputFixes(report) { 659 report.results.filter(result => Object.prototype.hasOwnProperty.call(result, "output")).forEach(result => { 660 fs.writeFileSync(result.filePath, result.output); 661 }); 662 } 663 664 665 /** 666 * Add a plugin by passing its configuration 667 * @param {string} name Name of the plugin. 668 * @param {Plugin} pluginObject Plugin configuration object. 669 * @returns {void} 670 */ 671 addPlugin(name, pluginObject) { 672 const { 673 additionalPluginPool, 674 configArrayFactory, 675 lastConfigArrays 676 } = internalSlotsMap.get(this); 677 678 additionalPluginPool.set(name, pluginObject); 679 configArrayFactory.clearCache(); 680 lastConfigArrays.length = 1; 681 lastConfigArrays[0] = configArrayFactory.getConfigArrayForFile(); 682 } 683 684 /** 685 * Resolves the patterns passed into executeOnFiles() into glob-based patterns 686 * for easier handling. 687 * @param {string[]} patterns The file patterns passed on the command line. 688 * @returns {string[]} The equivalent glob patterns. 689 */ 690 resolveFileGlobPatterns(patterns) { 691 const { options } = internalSlotsMap.get(this); 692 693 if (options.globInputPaths === false) { 694 return patterns.filter(Boolean); 695 } 696 697 const extensions = (options.extensions || [".js"]).map(ext => ext.replace(/^\./u, "")); 698 const dirSuffix = `/**/*.{${extensions.join(",")}}`; 699 700 return patterns.filter(Boolean).map(pathname => { 701 const resolvedPath = path.resolve(options.cwd, pathname); 702 const newPath = directoryExists(resolvedPath) 703 ? pathname.replace(/[/\\]$/u, "") + dirSuffix 704 : pathname; 705 706 return path.normalize(newPath).replace(/\\/gu, "/"); 707 }); 708 } 709 710 /** 711 * Executes the current configuration on an array of file and directory names. 712 * @param {string[]} patterns An array of file and directory names. 713 * @returns {LintReport} The results for all files that were linted. 714 */ 715 executeOnFiles(patterns) { 716 const { 717 cacheFilePath, 718 fileEnumerator, 719 lastConfigArrays, 720 lintResultCache, 721 linter, 722 options: { 723 allowInlineConfig, 724 cache, 725 cwd, 726 fix, 727 reportUnusedDisableDirectives 728 } 729 } = internalSlotsMap.get(this); 730 const results = []; 731 const startTime = Date.now(); 732 733 // Clear the last used config arrays. 734 lastConfigArrays.length = 0; 735 736 // Delete cache file; should this do here? 737 if (!cache) { 738 try { 739 fs.unlinkSync(cacheFilePath); 740 } catch (error) { 741 const errorCode = error && error.code; 742 743 // Ignore errors when no such file exists or file system is read only (and cache file does not exist) 744 if (errorCode !== "ENOENT" && !(errorCode === "EROFS" && !fs.existsSync(cacheFilePath))) { 745 throw error; 746 } 747 } 748 } 749 750 // Iterate source code files. 751 for (const { config, filePath, ignored } of fileEnumerator.iterateFiles(patterns)) { 752 if (ignored) { 753 results.push(createIgnoreResult(filePath, cwd)); 754 continue; 755 } 756 757 /* 758 * Store used configs for: 759 * - this method uses to collect used deprecated rules. 760 * - `getRules()` method uses to collect all loaded rules. 761 * - `--fix-type` option uses to get the loaded rule's meta data. 762 */ 763 if (!lastConfigArrays.includes(config)) { 764 lastConfigArrays.push(config); 765 } 766 767 // Skip if there is cached result. 768 if (lintResultCache) { 769 const cachedResult = 770 lintResultCache.getCachedLintResults(filePath, config); 771 772 if (cachedResult) { 773 const hadMessages = 774 cachedResult.messages && 775 cachedResult.messages.length > 0; 776 777 if (hadMessages && fix) { 778 debug(`Reprocessing cached file to allow autofix: ${filePath}`); 779 } else { 780 debug(`Skipping file since it hasn't changed: ${filePath}`); 781 results.push(cachedResult); 782 continue; 783 } 784 } 785 } 786 787 // Do lint. 788 const result = verifyText({ 789 text: fs.readFileSync(filePath, "utf8"), 790 filePath, 791 config, 792 cwd, 793 fix, 794 allowInlineConfig, 795 reportUnusedDisableDirectives, 796 fileEnumerator, 797 linter 798 }); 799 800 results.push(result); 801 802 /* 803 * Store the lint result in the LintResultCache. 804 * NOTE: The LintResultCache will remove the file source and any 805 * other properties that are difficult to serialize, and will 806 * hydrate those properties back in on future lint runs. 807 */ 808 if (lintResultCache) { 809 lintResultCache.setCachedLintResults(filePath, config, result); 810 } 811 } 812 813 // Persist the cache to disk. 814 if (lintResultCache) { 815 lintResultCache.reconcile(); 816 } 817 818 debug(`Linting complete in: ${Date.now() - startTime}ms`); 819 let usedDeprecatedRules; 820 821 return { 822 results, 823 ...calculateStatsPerRun(results), 824 825 // Initialize it lazily because CLI and `ESLint` API don't use it. 826 get usedDeprecatedRules() { 827 if (!usedDeprecatedRules) { 828 usedDeprecatedRules = Array.from( 829 iterateRuleDeprecationWarnings(lastConfigArrays) 830 ); 831 } 832 return usedDeprecatedRules; 833 } 834 }; 835 } 836 837 /** 838 * Executes the current configuration on text. 839 * @param {string} text A string of JavaScript code to lint. 840 * @param {string} [filename] An optional string representing the texts filename. 841 * @param {boolean} [warnIgnored] Always warn when a file is ignored 842 * @returns {LintReport} The results for the linting. 843 */ 844 executeOnText(text, filename, warnIgnored) { 845 const { 846 configArrayFactory, 847 fileEnumerator, 848 lastConfigArrays, 849 linter, 850 options: { 851 allowInlineConfig, 852 cwd, 853 fix, 854 reportUnusedDisableDirectives 855 } 856 } = internalSlotsMap.get(this); 857 const results = []; 858 const startTime = Date.now(); 859 const resolvedFilename = filename && path.resolve(cwd, filename); 860 861 862 // Clear the last used config arrays. 863 lastConfigArrays.length = 0; 864 if (resolvedFilename && this.isPathIgnored(resolvedFilename)) { 865 if (warnIgnored) { 866 results.push(createIgnoreResult(resolvedFilename, cwd)); 867 } 868 } else { 869 const config = configArrayFactory.getConfigArrayForFile( 870 resolvedFilename || "__placeholder__.js" 871 ); 872 873 /* 874 * Store used configs for: 875 * - this method uses to collect used deprecated rules. 876 * - `getRules()` method uses to collect all loaded rules. 877 * - `--fix-type` option uses to get the loaded rule's meta data. 878 */ 879 lastConfigArrays.push(config); 880 881 // Do lint. 882 results.push(verifyText({ 883 text, 884 filePath: resolvedFilename, 885 config, 886 cwd, 887 fix, 888 allowInlineConfig, 889 reportUnusedDisableDirectives, 890 fileEnumerator, 891 linter 892 })); 893 } 894 895 debug(`Linting complete in: ${Date.now() - startTime}ms`); 896 let usedDeprecatedRules; 897 898 return { 899 results, 900 ...calculateStatsPerRun(results), 901 902 // Initialize it lazily because CLI and `ESLint` API don't use it. 903 get usedDeprecatedRules() { 904 if (!usedDeprecatedRules) { 905 usedDeprecatedRules = Array.from( 906 iterateRuleDeprecationWarnings(lastConfigArrays) 907 ); 908 } 909 return usedDeprecatedRules; 910 } 911 }; 912 } 913 914 /** 915 * Returns a configuration object for the given file based on the CLI options. 916 * This is the same logic used by the ESLint CLI executable to determine 917 * configuration for each file it processes. 918 * @param {string} filePath The path of the file to retrieve a config object for. 919 * @returns {ConfigData} A configuration object for the file. 920 */ 921 getConfigForFile(filePath) { 922 const { configArrayFactory, options } = internalSlotsMap.get(this); 923 const absolutePath = path.resolve(options.cwd, filePath); 924 925 if (directoryExists(absolutePath)) { 926 throw Object.assign( 927 new Error("'filePath' should not be a directory path."), 928 { messageTemplate: "print-config-with-directory-path" } 929 ); 930 } 931 932 return configArrayFactory 933 .getConfigArrayForFile(absolutePath) 934 .extractConfig(absolutePath) 935 .toCompatibleObjectAsConfigFileContent(); 936 } 937 938 /** 939 * Checks if a given path is ignored by ESLint. 940 * @param {string} filePath The path of the file to check. 941 * @returns {boolean} Whether or not the given path is ignored. 942 */ 943 isPathIgnored(filePath) { 944 const { 945 configArrayFactory, 946 defaultIgnores, 947 options: { cwd, ignore } 948 } = internalSlotsMap.get(this); 949 const absolutePath = path.resolve(cwd, filePath); 950 951 if (ignore) { 952 const config = configArrayFactory 953 .getConfigArrayForFile(absolutePath) 954 .extractConfig(absolutePath); 955 const ignores = config.ignores || defaultIgnores; 956 957 return ignores(absolutePath); 958 } 959 960 return defaultIgnores(absolutePath); 961 } 962 963 /** 964 * Returns the formatter representing the given format or null if the `format` is not a string. 965 * @param {string} [format] The name of the format to load or the path to a 966 * custom formatter. 967 * @returns {(Function|null)} The formatter function or null if the `format` is not a string. 968 */ 969 getFormatter(format) { 970 971 // default is stylish 972 const resolvedFormatName = format || "stylish"; 973 974 // only strings are valid formatters 975 if (typeof resolvedFormatName === "string") { 976 977 // replace \ with / for Windows compatibility 978 const normalizedFormatName = resolvedFormatName.replace(/\\/gu, "/"); 979 980 const slots = internalSlotsMap.get(this); 981 const cwd = slots ? slots.options.cwd : process.cwd(); 982 const namespace = naming.getNamespaceFromTerm(normalizedFormatName); 983 984 let formatterPath; 985 986 // if there's a slash, then it's a file (TODO: this check seems dubious for scoped npm packages) 987 if (!namespace && normalizedFormatName.indexOf("/") > -1) { 988 formatterPath = path.resolve(cwd, normalizedFormatName); 989 } else { 990 try { 991 const npmFormat = naming.normalizePackageName(normalizedFormatName, "eslint-formatter"); 992 993 formatterPath = ModuleResolver.resolve(npmFormat, path.join(cwd, "__placeholder__.js")); 994 } catch { 995 formatterPath = path.resolve(__dirname, "formatters", normalizedFormatName); 996 } 997 } 998 999 try { 1000 return require(formatterPath); 1001 } catch (ex) { 1002 ex.message = `There was a problem loading formatter: ${formatterPath}\nError: ${ex.message}`; 1003 throw ex; 1004 } 1005 1006 } else { 1007 return null; 1008 } 1009 } 1010} 1011 1012CLIEngine.version = pkg.version; 1013CLIEngine.getFormatter = CLIEngine.prototype.getFormatter; 1014 1015module.exports = { 1016 CLIEngine, 1017 1018 /** 1019 * Get the internal slots of a given CLIEngine instance for tests. 1020 * @param {CLIEngine} instance The CLIEngine instance to get. 1021 * @returns {CLIEngineInternalSlots} The internal slots. 1022 */ 1023 getCLIEngineInternalSlots(instance) { 1024 return internalSlotsMap.get(instance); 1025 } 1026}; 1027