Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

696 строки
24 KiB

  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. path = require("path"),
  16. defaultOptions = require("../conf/default-cli-options"),
  17. Linter = require("./linter"),
  18. IgnoredPaths = require("./ignored-paths"),
  19. Config = require("./config"),
  20. LintResultCache = require("./util/lint-result-cache"),
  21. globUtils = require("./util/glob-utils"),
  22. validator = require("./config/config-validator"),
  23. hash = require("./util/hash"),
  24. ModuleResolver = require("./util/module-resolver"),
  25. naming = require("./util/naming"),
  26. pkg = require("../package.json");
  27. const debug = require("debug")("eslint:cli-engine");
  28. const resolver = new ModuleResolver();
  29. //------------------------------------------------------------------------------
  30. // Typedefs
  31. //------------------------------------------------------------------------------
  32. /**
  33. * The options to configure a CLI engine with.
  34. * @typedef {Object} CLIEngineOptions
  35. * @property {boolean} allowInlineConfig Enable or disable inline configuration comments.
  36. * @property {Object} baseConfig Base config object, extended by all configs used with this CLIEngine instance
  37. * @property {boolean} cache Enable result caching.
  38. * @property {string} cacheLocation The cache file to use instead of .eslintcache.
  39. * @property {string} configFile The configuration file to use.
  40. * @property {string} cwd The value to use for the current working directory.
  41. * @property {string[]} envs An array of environments to load.
  42. * @property {string[]} extensions An array of file extensions to check.
  43. * @property {boolean|Function} fix Execute in autofix mode. If a function, should return a boolean.
  44. * @property {string[]} globals An array of global variables to declare.
  45. * @property {boolean} ignore False disables use of .eslintignore.
  46. * @property {string} ignorePath The ignore file to use instead of .eslintignore.
  47. * @property {string} ignorePattern A glob pattern of files to ignore.
  48. * @property {boolean} useEslintrc False disables looking for .eslintrc
  49. * @property {string} parser The name of the parser to use.
  50. * @property {Object} parserOptions An object of parserOption settings to use.
  51. * @property {string[]} plugins An array of plugins to load.
  52. * @property {Object<string,*>} rules An object of rules to use.
  53. * @property {string[]} rulePaths An array of directories to load custom rules from.
  54. * @property {boolean} reportUnusedDisableDirectives `true` adds reports for unused eslint-disable directives
  55. */
  56. /**
  57. * A linting warning or error.
  58. * @typedef {Object} LintMessage
  59. * @property {string} message The message to display to the user.
  60. */
  61. /**
  62. * A linting result.
  63. * @typedef {Object} LintResult
  64. * @property {string} filePath The path to the file that was linted.
  65. * @property {LintMessage[]} messages All of the messages for the result.
  66. * @property {number} errorCount Number of errors for the result.
  67. * @property {number} warningCount Number of warnings for the result.
  68. * @property {number} fixableErrorCount Number of fixable errors for the result.
  69. * @property {number} fixableWarningCount Number of fixable warnings for the result.
  70. * @property {string=} [source] The source code of the file that was linted.
  71. * @property {string=} [output] The source code of the file that was linted, with as many fixes applied as possible.
  72. */
  73. //------------------------------------------------------------------------------
  74. // Helpers
  75. //------------------------------------------------------------------------------
  76. /**
  77. * It will calculate the error and warning count for collection of messages per file
  78. * @param {Object[]} messages - Collection of messages
  79. * @returns {Object} Contains the stats
  80. * @private
  81. */
  82. function calculateStatsPerFile(messages) {
  83. return messages.reduce((stat, message) => {
  84. if (message.fatal || message.severity === 2) {
  85. stat.errorCount++;
  86. if (message.fix) {
  87. stat.fixableErrorCount++;
  88. }
  89. } else {
  90. stat.warningCount++;
  91. if (message.fix) {
  92. stat.fixableWarningCount++;
  93. }
  94. }
  95. return stat;
  96. }, {
  97. errorCount: 0,
  98. warningCount: 0,
  99. fixableErrorCount: 0,
  100. fixableWarningCount: 0
  101. });
  102. }
  103. /**
  104. * It will calculate the error and warning count for collection of results from all files
  105. * @param {Object[]} results - Collection of messages from all the files
  106. * @returns {Object} Contains the stats
  107. * @private
  108. */
  109. function calculateStatsPerRun(results) {
  110. return results.reduce((stat, result) => {
  111. stat.errorCount += result.errorCount;
  112. stat.warningCount += result.warningCount;
  113. stat.fixableErrorCount += result.fixableErrorCount;
  114. stat.fixableWarningCount += result.fixableWarningCount;
  115. return stat;
  116. }, {
  117. errorCount: 0,
  118. warningCount: 0,
  119. fixableErrorCount: 0,
  120. fixableWarningCount: 0
  121. });
  122. }
  123. /**
  124. * Processes an source code using ESLint.
  125. * @param {string} text The source code to check.
  126. * @param {Object} configHelper The configuration options for ESLint.
  127. * @param {string} filename An optional string representing the texts filename.
  128. * @param {boolean|Function} fix Indicates if fixes should be processed.
  129. * @param {boolean} allowInlineConfig Allow/ignore comments that change config.
  130. * @param {boolean} reportUnusedDisableDirectives Allow/ignore comments that change config.
  131. * @param {Linter} linter Linter context
  132. * @returns {LintResult} The results for linting on this text.
  133. * @private
  134. */
  135. function processText(text, configHelper, filename, fix, allowInlineConfig, reportUnusedDisableDirectives, linter) {
  136. let filePath,
  137. fileExtension,
  138. processor;
  139. if (filename) {
  140. filePath = path.resolve(filename);
  141. fileExtension = path.extname(filename);
  142. }
  143. const effectiveFilename = filename || "<text>";
  144. debug(`Linting ${effectiveFilename}`);
  145. const config = configHelper.getConfig(filePath);
  146. if (config.plugins) {
  147. configHelper.plugins.loadAll(config.plugins);
  148. }
  149. const loadedPlugins = configHelper.plugins.getAll();
  150. for (const plugin in loadedPlugins) {
  151. if (loadedPlugins[plugin].processors && Object.keys(loadedPlugins[plugin].processors).indexOf(fileExtension) >= 0) {
  152. processor = loadedPlugins[plugin].processors[fileExtension];
  153. break;
  154. }
  155. }
  156. const autofixingEnabled = typeof fix !== "undefined" && (!processor || processor.supportsAutofix);
  157. const fixedResult = linter.verifyAndFix(text, config, {
  158. filename: effectiveFilename,
  159. allowInlineConfig,
  160. reportUnusedDisableDirectives,
  161. fix: !!autofixingEnabled && fix,
  162. preprocess: processor && (rawText => processor.preprocess(rawText, effectiveFilename)),
  163. postprocess: processor && (problemLists => processor.postprocess(problemLists, effectiveFilename))
  164. });
  165. const stats = calculateStatsPerFile(fixedResult.messages);
  166. const result = {
  167. filePath: effectiveFilename,
  168. messages: fixedResult.messages,
  169. errorCount: stats.errorCount,
  170. warningCount: stats.warningCount,
  171. fixableErrorCount: stats.fixableErrorCount,
  172. fixableWarningCount: stats.fixableWarningCount
  173. };
  174. if (fixedResult.fixed) {
  175. result.output = fixedResult.output;
  176. }
  177. if (result.errorCount + result.warningCount > 0 && typeof result.output === "undefined") {
  178. result.source = text;
  179. }
  180. return result;
  181. }
  182. /**
  183. * Processes an individual file using ESLint. Files used here are known to
  184. * exist, so no need to check that here.
  185. * @param {string} filename The filename of the file being checked.
  186. * @param {Object} configHelper The configuration options for ESLint.
  187. * @param {Object} options The CLIEngine options object.
  188. * @param {Linter} linter Linter context
  189. * @returns {LintResult} The results for linting on this file.
  190. * @private
  191. */
  192. function processFile(filename, configHelper, options, linter) {
  193. const text = fs.readFileSync(path.resolve(filename), "utf8"),
  194. result = processText(
  195. text,
  196. configHelper,
  197. filename,
  198. options.fix,
  199. options.allowInlineConfig,
  200. options.reportUnusedDisableDirectives,
  201. linter
  202. );
  203. return result;
  204. }
  205. /**
  206. * Returns result with warning by ignore settings
  207. * @param {string} filePath - File path of checked code
  208. * @param {string} baseDir - Absolute path of base directory
  209. * @returns {LintResult} Result with single warning
  210. * @private
  211. */
  212. function createIgnoreResult(filePath, baseDir) {
  213. let message;
  214. const isHidden = /^\./.test(path.basename(filePath));
  215. const isInNodeModules = baseDir && path.relative(baseDir, filePath).startsWith("node_modules");
  216. const isInBowerComponents = baseDir && path.relative(baseDir, filePath).startsWith("bower_components");
  217. if (isHidden) {
  218. message = "File ignored by default. Use a negated ignore pattern (like \"--ignore-pattern '!<relative/path/to/filename>'\") to override.";
  219. } else if (isInNodeModules) {
  220. message = "File ignored by default. Use \"--ignore-pattern '!node_modules/*'\" to override.";
  221. } else if (isInBowerComponents) {
  222. message = "File ignored by default. Use \"--ignore-pattern '!bower_components/*'\" to override.";
  223. } else {
  224. message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override.";
  225. }
  226. return {
  227. filePath: path.resolve(filePath),
  228. messages: [
  229. {
  230. fatal: false,
  231. severity: 1,
  232. message
  233. }
  234. ],
  235. errorCount: 0,
  236. warningCount: 1,
  237. fixableErrorCount: 0,
  238. fixableWarningCount: 0
  239. };
  240. }
  241. /**
  242. * Checks if the given message is an error message.
  243. * @param {Object} message The message to check.
  244. * @returns {boolean} Whether or not the message is an error message.
  245. * @private
  246. */
  247. function isErrorMessage(message) {
  248. return message.severity === 2;
  249. }
  250. /**
  251. * return the cacheFile to be used by eslint, based on whether the provided parameter is
  252. * a directory or looks like a directory (ends in `path.sep`), in which case the file
  253. * name will be the `cacheFile/.cache_hashOfCWD`
  254. *
  255. * if cacheFile points to a file or looks like a file then in will just use that file
  256. *
  257. * @param {string} cacheFile The name of file to be used to store the cache
  258. * @param {string} cwd Current working directory
  259. * @returns {string} the resolved path to the cache file
  260. */
  261. function getCacheFile(cacheFile, cwd) {
  262. /*
  263. * make sure the path separators are normalized for the environment/os
  264. * keeping the trailing path separator if present
  265. */
  266. const normalizedCacheFile = path.normalize(cacheFile);
  267. const resolvedCacheFile = path.resolve(cwd, normalizedCacheFile);
  268. const looksLikeADirectory = normalizedCacheFile.slice(-1) === path.sep;
  269. /**
  270. * return the name for the cache file in case the provided parameter is a directory
  271. * @returns {string} the resolved path to the cacheFile
  272. */
  273. function getCacheFileForDirectory() {
  274. return path.join(resolvedCacheFile, `.cache_${hash(cwd)}`);
  275. }
  276. let fileStats;
  277. try {
  278. fileStats = fs.lstatSync(resolvedCacheFile);
  279. } catch (ex) {
  280. fileStats = null;
  281. }
  282. /*
  283. * in case the file exists we need to verify if the provided path
  284. * is a directory or a file. If it is a directory we want to create a file
  285. * inside that directory
  286. */
  287. if (fileStats) {
  288. /*
  289. * is a directory or is a file, but the original file the user provided
  290. * looks like a directory but `path.resolve` removed the `last path.sep`
  291. * so we need to still treat this like a directory
  292. */
  293. if (fileStats.isDirectory() || looksLikeADirectory) {
  294. return getCacheFileForDirectory();
  295. }
  296. // is file so just use that file
  297. return resolvedCacheFile;
  298. }
  299. /*
  300. * here we known the file or directory doesn't exist,
  301. * so we will try to infer if its a directory if it looks like a directory
  302. * for the current operating system.
  303. */
  304. // if the last character passed is a path separator we assume is a directory
  305. if (looksLikeADirectory) {
  306. return getCacheFileForDirectory();
  307. }
  308. return resolvedCacheFile;
  309. }
  310. //------------------------------------------------------------------------------
  311. // Public Interface
  312. //------------------------------------------------------------------------------
  313. class CLIEngine {
  314. /**
  315. * Creates a new instance of the core CLI engine.
  316. * @param {CLIEngineOptions} providedOptions The options for this instance.
  317. * @constructor
  318. */
  319. constructor(providedOptions) {
  320. const options = Object.assign(
  321. Object.create(null),
  322. defaultOptions,
  323. { cwd: process.cwd() },
  324. providedOptions
  325. );
  326. /*
  327. * if an --ignore-path option is provided, ensure that the ignore
  328. * file exists and is not a directory
  329. */
  330. if (options.ignore && options.ignorePath) {
  331. try {
  332. if (!fs.statSync(options.ignorePath).isFile()) {
  333. throw new Error(`${options.ignorePath} is not a file`);
  334. }
  335. } catch (e) {
  336. e.message = `Error: Could not load file ${options.ignorePath}\nError: ${e.message}`;
  337. throw e;
  338. }
  339. }
  340. /**
  341. * Stored options for this instance
  342. * @type {Object}
  343. */
  344. this.options = options;
  345. this.linter = new Linter();
  346. // load in additional rules
  347. if (this.options.rulePaths) {
  348. const cwd = this.options.cwd;
  349. this.options.rulePaths.forEach(rulesdir => {
  350. debug(`Loading rules from ${rulesdir}`);
  351. this.linter.rules.load(rulesdir, cwd);
  352. });
  353. }
  354. if (this.options.rules && Object.keys(this.options.rules).length) {
  355. const loadedRules = this.linter.getRules();
  356. Object.keys(this.options.rules).forEach(name => {
  357. validator.validateRuleOptions(loadedRules.get(name), name, this.options.rules[name], "CLI");
  358. });
  359. }
  360. this.config = new Config(this.options, this.linter);
  361. if (this.options.cache) {
  362. const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd);
  363. /**
  364. * Cache used to avoid operating on files that haven't changed since the
  365. * last successful execution.
  366. * @type {Object}
  367. */
  368. this._lintResultCache = new LintResultCache(cacheFile, this.config);
  369. }
  370. }
  371. getRules() {
  372. return this.linter.getRules();
  373. }
  374. /**
  375. * Returns results that only contains errors.
  376. * @param {LintResult[]} results The results to filter.
  377. * @returns {LintResult[]} The filtered results.
  378. */
  379. static getErrorResults(results) {
  380. const filtered = [];
  381. results.forEach(result => {
  382. const filteredMessages = result.messages.filter(isErrorMessage);
  383. if (filteredMessages.length > 0) {
  384. filtered.push(
  385. Object.assign(result, {
  386. messages: filteredMessages,
  387. errorCount: filteredMessages.length,
  388. warningCount: 0,
  389. fixableErrorCount: result.fixableErrorCount,
  390. fixableWarningCount: 0
  391. })
  392. );
  393. }
  394. });
  395. return filtered;
  396. }
  397. /**
  398. * Outputs fixes from the given results to files.
  399. * @param {Object} report The report object created by CLIEngine.
  400. * @returns {void}
  401. */
  402. static outputFixes(report) {
  403. report.results.filter(result => Object.prototype.hasOwnProperty.call(result, "output")).forEach(result => {
  404. fs.writeFileSync(result.filePath, result.output);
  405. });
  406. }
  407. /**
  408. * Add a plugin by passing its configuration
  409. * @param {string} name Name of the plugin.
  410. * @param {Object} pluginobject Plugin configuration object.
  411. * @returns {void}
  412. */
  413. addPlugin(name, pluginobject) {
  414. this.config.plugins.define(name, pluginobject);
  415. }
  416. /**
  417. * Resolves the patterns passed into executeOnFiles() into glob-based patterns
  418. * for easier handling.
  419. * @param {string[]} patterns The file patterns passed on the command line.
  420. * @returns {string[]} The equivalent glob patterns.
  421. */
  422. resolveFileGlobPatterns(patterns) {
  423. return globUtils.resolveFileGlobPatterns(patterns.filter(Boolean), this.options);
  424. }
  425. /**
  426. * Executes the current configuration on an array of file and directory names.
  427. * @param {string[]} patterns An array of file and directory names.
  428. * @returns {Object} The results for all files that were linted.
  429. */
  430. executeOnFiles(patterns) {
  431. const options = this.options,
  432. lintResultCache = this._lintResultCache,
  433. configHelper = this.config;
  434. const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd);
  435. if (!options.cache && fs.existsSync(cacheFile)) {
  436. fs.unlinkSync(cacheFile);
  437. }
  438. const startTime = Date.now();
  439. const fileList = globUtils.listFilesToProcess(patterns, options);
  440. const results = fileList.map(fileInfo => {
  441. if (fileInfo.ignored) {
  442. return createIgnoreResult(fileInfo.filename, options.cwd);
  443. }
  444. if (options.cache) {
  445. const cachedLintResults = lintResultCache.getCachedLintResults(fileInfo.filename);
  446. if (cachedLintResults) {
  447. const resultHadMessages = cachedLintResults.messages && cachedLintResults.messages.length;
  448. if (resultHadMessages && options.fix) {
  449. debug(`Reprocessing cached file to allow autofix: ${fileInfo.filename}`);
  450. } else {
  451. debug(`Skipping file since it hasn't changed: ${fileInfo.filename}`);
  452. return cachedLintResults;
  453. }
  454. }
  455. }
  456. debug(`Processing ${fileInfo.filename}`);
  457. return processFile(fileInfo.filename, configHelper, options, this.linter);
  458. });
  459. if (options.cache) {
  460. results.forEach(result => {
  461. /*
  462. * Store the lint result in the LintResultCache.
  463. * NOTE: The LintResultCache will remove the file source and any
  464. * other properties that are difficult to serialize, and will
  465. * hydrate those properties back in on future lint runs.
  466. */
  467. lintResultCache.setCachedLintResults(result.filePath, result);
  468. });
  469. // persist the cache to disk
  470. lintResultCache.reconcile();
  471. }
  472. const stats = calculateStatsPerRun(results);
  473. debug(`Linting complete in: ${Date.now() - startTime}ms`);
  474. return {
  475. results,
  476. errorCount: stats.errorCount,
  477. warningCount: stats.warningCount,
  478. fixableErrorCount: stats.fixableErrorCount,
  479. fixableWarningCount: stats.fixableWarningCount
  480. };
  481. }
  482. /**
  483. * Executes the current configuration on text.
  484. * @param {string} text A string of JavaScript code to lint.
  485. * @param {string} filename An optional string representing the texts filename.
  486. * @param {boolean} warnIgnored Always warn when a file is ignored
  487. * @returns {Object} The results for the linting.
  488. */
  489. executeOnText(text, filename, warnIgnored) {
  490. const results = [],
  491. options = this.options,
  492. configHelper = this.config,
  493. ignoredPaths = new IgnoredPaths(options);
  494. // resolve filename based on options.cwd (for reporting, ignoredPaths also resolves)
  495. const resolvedFilename = filename && !path.isAbsolute(filename)
  496. ? path.resolve(options.cwd, filename)
  497. : filename;
  498. if (resolvedFilename && ignoredPaths.contains(resolvedFilename)) {
  499. if (warnIgnored) {
  500. results.push(createIgnoreResult(resolvedFilename, options.cwd));
  501. }
  502. } else {
  503. results.push(
  504. processText(
  505. text,
  506. configHelper,
  507. resolvedFilename,
  508. options.fix,
  509. options.allowInlineConfig,
  510. options.reportUnusedDisableDirectives,
  511. this.linter
  512. )
  513. );
  514. }
  515. const stats = calculateStatsPerRun(results);
  516. return {
  517. results,
  518. errorCount: stats.errorCount,
  519. warningCount: stats.warningCount,
  520. fixableErrorCount: stats.fixableErrorCount,
  521. fixableWarningCount: stats.fixableWarningCount
  522. };
  523. }
  524. /**
  525. * Returns a configuration object for the given file based on the CLI options.
  526. * This is the same logic used by the ESLint CLI executable to determine
  527. * configuration for each file it processes.
  528. * @param {string} filePath The path of the file to retrieve a config object for.
  529. * @returns {Object} A configuration object for the file.
  530. */
  531. getConfigForFile(filePath) {
  532. const configHelper = this.config;
  533. return configHelper.getConfig(filePath);
  534. }
  535. /**
  536. * Checks if a given path is ignored by ESLint.
  537. * @param {string} filePath The path of the file to check.
  538. * @returns {boolean} Whether or not the given path is ignored.
  539. */
  540. isPathIgnored(filePath) {
  541. const resolvedPath = path.resolve(this.options.cwd, filePath);
  542. const ignoredPaths = new IgnoredPaths(this.options);
  543. return ignoredPaths.contains(resolvedPath);
  544. }
  545. /**
  546. * Returns the formatter representing the given format or null if no formatter
  547. * with the given name can be found.
  548. * @param {string} [format] The name of the format to load or the path to a
  549. * custom formatter.
  550. * @returns {Function} The formatter function or null if not found.
  551. */
  552. getFormatter(format) {
  553. // default is stylish
  554. const resolvedFormatName = format || "stylish";
  555. // only strings are valid formatters
  556. if (typeof resolvedFormatName === "string") {
  557. // replace \ with / for Windows compatibility
  558. const normalizedFormatName = resolvedFormatName.replace(/\\/g, "/");
  559. const cwd = this.options ? this.options.cwd : process.cwd();
  560. const namespace = naming.getNamespaceFromTerm(normalizedFormatName);
  561. let formatterPath;
  562. // if there's a slash, then it's a file
  563. if (!namespace && normalizedFormatName.indexOf("/") > -1) {
  564. formatterPath = path.resolve(cwd, normalizedFormatName);
  565. } else {
  566. try {
  567. const npmFormat = naming.normalizePackageName(normalizedFormatName, "eslint-formatter");
  568. formatterPath = resolver.resolve(npmFormat, `${cwd}/node_modules`);
  569. } catch (e) {
  570. formatterPath = `./formatters/${normalizedFormatName}`;
  571. }
  572. }
  573. try {
  574. return require(formatterPath);
  575. } catch (ex) {
  576. ex.message = `There was a problem loading formatter: ${formatterPath}\nError: ${ex.message}`;
  577. throw ex;
  578. }
  579. } else {
  580. return null;
  581. }
  582. }
  583. }
  584. CLIEngine.version = pkg.version;
  585. CLIEngine.getFormatter = CLIEngine.prototype.getFormatter;
  586. module.exports = CLIEngine;