cli-engine.js 35 KB

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