1 | /**
|
2 | * @fileoverview Main CLI object.
|
3 | * @author Nicholas C. Zakas
|
4 | */
|
5 |
|
6 | ;
|
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 |
|
18 | const fs = require("fs"),
|
19 | path = require("path"),
|
20 | defaultOptions = require("../conf/default-cli-options"),
|
21 | Linter = require("./linter"),
|
22 | IgnoredPaths = require("./ignored-paths"),
|
23 | Config = require("./config"),
|
24 | fileEntryCache = require("file-entry-cache"),
|
25 | globUtil = require("./util/glob-util"),
|
26 | SourceCodeFixer = require("./util/source-code-fixer"),
|
27 | validator = require("./config/config-validator"),
|
28 | stringify = require("json-stable-stringify"),
|
29 | hash = require("./util/hash"),
|
30 |
|
31 | pkg = require("../package.json");
|
32 |
|
33 | const debug = require("debug")("eslint:cli-engine");
|
34 |
|
35 | //------------------------------------------------------------------------------
|
36 | // Typedefs
|
37 | //------------------------------------------------------------------------------
|
38 |
|
39 | /**
|
40 | * The options to configure a CLI engine with.
|
41 | * @typedef {Object} CLIEngineOptions
|
42 | * @property {boolean} allowInlineConfig Enable or disable inline configuration comments.
|
43 | * @property {boolean|Object} baseConfig Base config object. True enables recommend rules and environments.
|
44 | * @property {boolean} cache Enable result caching.
|
45 | * @property {string} cacheLocation The cache file to use instead of .eslintcache.
|
46 | * @property {string} configFile The configuration file to use.
|
47 | * @property {string} cwd The value to use for the current working directory.
|
48 | * @property {string[]} envs An array of environments to load.
|
49 | * @property {string[]} extensions An array of file extensions to check.
|
50 | * @property {boolean} fix Execute in autofix mode.
|
51 | * @property {string[]} globals An array of global variables to declare.
|
52 | * @property {boolean} ignore False disables use of .eslintignore.
|
53 | * @property {string} ignorePath The ignore file to use instead of .eslintignore.
|
54 | * @property {string} ignorePattern A glob pattern of files to ignore.
|
55 | * @property {boolean} useEslintrc False disables looking for .eslintrc
|
56 | * @property {string} parser The name of the parser to use.
|
57 | * @property {Object} parserOptions An object of parserOption settings to use.
|
58 | * @property {string[]} plugins An array of plugins to load.
|
59 | * @property {Object<string,*>} rules An object of rules to use.
|
60 | * @property {string[]} rulePaths An array of directories to load custom rules from.
|
61 | */
|
62 |
|
63 | /**
|
64 | * A linting warning or error.
|
65 | * @typedef {Object} LintMessage
|
66 | * @property {string} message The message to display to the user.
|
67 | */
|
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 | //------------------------------------------------------------------------------
|
83 | // Helpers
|
84 | //------------------------------------------------------------------------------
|
85 |
|
86 | /**
|
87 | * It will calculate the error and warning count for collection of messages per file
|
88 | * @param {Object[]} messages - Collection of messages
|
89 | * @returns {Object} Contains the stats
|
90 | * @private
|
91 | */
|
92 | function calculateStatsPerFile(messages) {
|
93 | return messages.reduce((stat, message) => {
|
94 | if (message.fatal || message.severity === 2) {
|
95 | stat.errorCount++;
|
96 | if (message.fix) {
|
97 | stat.fixableErrorCount++;
|
98 | }
|
99 | } else {
|
100 | stat.warningCount++;
|
101 | if (message.fix) {
|
102 | stat.fixableWarningCount++;
|
103 | }
|
104 | }
|
105 | return stat;
|
106 | }, {
|
107 | errorCount: 0,
|
108 | warningCount: 0,
|
109 | fixableErrorCount: 0,
|
110 | fixableWarningCount: 0
|
111 | });
|
112 | }
|
113 |
|
114 | /**
|
115 | * It will calculate the error and warning count for collection of results from all files
|
116 | * @param {Object[]} results - Collection of messages from all the files
|
117 | * @returns {Object} Contains the stats
|
118 | * @private
|
119 | */
|
120 | function calculateStatsPerRun(results) {
|
121 | return results.reduce((stat, result) => {
|
122 | stat.errorCount += result.errorCount;
|
123 | stat.warningCount += result.warningCount;
|
124 | stat.fixableErrorCount += result.fixableErrorCount;
|
125 | stat.fixableWarningCount += result.fixableWarningCount;
|
126 | return stat;
|
127 | }, {
|
128 | errorCount: 0,
|
129 | warningCount: 0,
|
130 | fixableErrorCount: 0,
|
131 | fixableWarningCount: 0
|
132 | });
|
133 | }
|
134 |
|
135 | /**
|
136 | * Performs multiple autofix passes over the text until as many fixes as possible
|
137 | * have been applied.
|
138 | * @param {string} text The source text to apply fixes to.
|
139 | * @param {Object} config The ESLint config object to use.
|
140 | * @param {Object} options The ESLint options object to use.
|
141 | * @param {string} options.filename The filename from which the text was read.
|
142 | * @param {boolean} options.allowInlineConfig Flag indicating if inline comments
|
143 | * should be allowed.
|
144 | * @param {Linter} linter Linter context
|
145 | * @returns {Object} The result of the fix operation as returned from the
|
146 | * SourceCodeFixer.
|
147 | * @private
|
148 | */
|
149 | function multipassFix(text, config, options, linter) {
|
150 | const MAX_PASSES = 10;
|
151 | let messages = [],
|
152 | fixedResult,
|
153 | fixed = false,
|
154 | passNumber = 0;
|
155 |
|
156 | /**
|
157 | * This loop continues until one of the following is true:
|
158 | *
|
159 | * 1. No more fixes have been applied.
|
160 | * 2. Ten passes have been made.
|
161 | *
|
162 | * That means anytime a fix is successfully applied, there will be another pass.
|
163 | * Essentially, guaranteeing a minimum of two passes.
|
164 | */
|
165 | do {
|
166 | passNumber++;
|
167 |
|
168 | debug(`Linting code for ${options.filename} (pass ${passNumber})`);
|
169 | messages = linter.verify(text, config, options);
|
170 |
|
171 | debug(`Generating fixed text for ${options.filename} (pass ${passNumber})`);
|
172 | fixedResult = SourceCodeFixer.applyFixes(linter.getSourceCode(), messages);
|
173 |
|
174 | // stop if there are any syntax errors.
|
175 | // 'fixedResult.output' is a empty string.
|
176 | if (messages.length === 1 && messages[0].fatal) {
|
177 | break;
|
178 | }
|
179 |
|
180 | // keep track if any fixes were ever applied - important for return value
|
181 | fixed = fixed || fixedResult.fixed;
|
182 |
|
183 | // update to use the fixed output instead of the original text
|
184 | text = fixedResult.output;
|
185 |
|
186 | } while (
|
187 | fixedResult.fixed &&
|
188 | passNumber < MAX_PASSES
|
189 | );
|
190 |
|
191 |
|
192 | /*
|
193 | * If the last result had fixes, we need to lint again to be sure we have
|
194 | * the most up-to-date information.
|
195 | */
|
196 | if (fixedResult.fixed) {
|
197 | fixedResult.messages = linter.verify(text, config, options);
|
198 | }
|
199 |
|
200 |
|
201 | // ensure the last result properly reflects if fixes were done
|
202 | fixedResult.fixed = fixed;
|
203 | fixedResult.output = text;
|
204 |
|
205 | return fixedResult;
|
206 |
|
207 | }
|
208 |
|
209 | /**
|
210 | * Processes an source code using ESLint.
|
211 | * @param {string} text The source code to check.
|
212 | * @param {Object} configHelper The configuration options for ESLint.
|
213 | * @param {string} filename An optional string representing the texts filename.
|
214 | * @param {boolean} fix Indicates if fixes should be processed.
|
215 | * @param {boolean} allowInlineConfig Allow/ignore comments that change config.
|
216 | * @param {Linter} linter Linter context
|
217 | * @returns {LintResult} The results for linting on this text.
|
218 | * @private
|
219 | */
|
220 | function processText(text, configHelper, filename, fix, allowInlineConfig, linter) {
|
221 |
|
222 | // clear all existing settings for a new file
|
223 | linter.reset();
|
224 |
|
225 | let filePath,
|
226 | messages,
|
227 | fileExtension,
|
228 | processor,
|
229 | fixedResult;
|
230 |
|
231 | if (filename) {
|
232 | filePath = path.resolve(filename);
|
233 | fileExtension = path.extname(filename);
|
234 | }
|
235 |
|
236 | filename = filename || "<text>";
|
237 | debug(`Linting ${filename}`);
|
238 | const config = configHelper.getConfig(filePath);
|
239 |
|
240 | if (config.plugins) {
|
241 | configHelper.plugins.loadAll(config.plugins);
|
242 | }
|
243 |
|
244 | const loadedPlugins = configHelper.plugins.getAll();
|
245 |
|
246 | for (const plugin in loadedPlugins) {
|
247 | if (loadedPlugins[plugin].processors && Object.keys(loadedPlugins[plugin].processors).indexOf(fileExtension) >= 0) {
|
248 | processor = loadedPlugins[plugin].processors[fileExtension];
|
249 | break;
|
250 | }
|
251 | }
|
252 |
|
253 | if (processor) {
|
254 | debug("Using processor");
|
255 | const parsedBlocks = processor.preprocess(text, filename);
|
256 | const unprocessedMessages = [];
|
257 |
|
258 | parsedBlocks.forEach(block => {
|
259 | unprocessedMessages.push(linter.verify(block, config, {
|
260 | filename,
|
261 | allowInlineConfig
|
262 | }));
|
263 | });
|
264 |
|
265 | // TODO(nzakas): Figure out how fixes might work for processors
|
266 |
|
267 | messages = processor.postprocess(unprocessedMessages, filename);
|
268 |
|
269 | } else {
|
270 |
|
271 | if (fix) {
|
272 | fixedResult = multipassFix(text, config, {
|
273 | filename,
|
274 | allowInlineConfig
|
275 | }, linter);
|
276 | messages = fixedResult.messages;
|
277 | } else {
|
278 | messages = linter.verify(text, config, {
|
279 | filename,
|
280 | allowInlineConfig
|
281 | });
|
282 | }
|
283 | }
|
284 |
|
285 | const stats = calculateStatsPerFile(messages);
|
286 |
|
287 | const result = {
|
288 | filePath: filename,
|
289 | messages,
|
290 | errorCount: stats.errorCount,
|
291 | warningCount: stats.warningCount,
|
292 | fixableErrorCount: stats.fixableErrorCount,
|
293 | fixableWarningCount: stats.fixableWarningCount
|
294 | };
|
295 |
|
296 | if (fixedResult && fixedResult.fixed) {
|
297 | result.output = fixedResult.output;
|
298 | }
|
299 |
|
300 | if (result.errorCount + result.warningCount > 0 && typeof result.output === "undefined") {
|
301 | result.source = text;
|
302 | }
|
303 |
|
304 | return result;
|
305 | }
|
306 |
|
307 | /**
|
308 | * Processes an individual file using ESLint. Files used here are known to
|
309 | * exist, so no need to check that here.
|
310 | * @param {string} filename The filename of the file being checked.
|
311 | * @param {Object} configHelper The configuration options for ESLint.
|
312 | * @param {Object} options The CLIEngine options object.
|
313 | * @param {Linter} linter Linter context
|
314 | * @returns {LintResult} The results for linting on this file.
|
315 | * @private
|
316 | */
|
317 | function processFile(filename, configHelper, options, linter) {
|
318 |
|
319 | const text = fs.readFileSync(path.resolve(filename), "utf8"),
|
320 | result = processText(text, configHelper, filename, options.fix, options.allowInlineConfig, linter);
|
321 |
|
322 | return result;
|
323 |
|
324 | }
|
325 |
|
326 | /**
|
327 | * Returns result with warning by ignore settings
|
328 | * @param {string} filePath - File path of checked code
|
329 | * @param {string} baseDir - Absolute path of base directory
|
330 | * @returns {LintResult} Result with single warning
|
331 | * @private
|
332 | */
|
333 | function createIgnoreResult(filePath, baseDir) {
|
334 | let message;
|
335 | const isHidden = /^\./.test(path.basename(filePath));
|
336 | const isInNodeModules = baseDir && /^node_modules/.test(path.relative(baseDir, filePath));
|
337 | const isInBowerComponents = baseDir && /^bower_components/.test(path.relative(baseDir, filePath));
|
338 |
|
339 | if (isHidden) {
|
340 | message = "File ignored by default. Use a negated ignore pattern (like \"--ignore-pattern '!<relative/path/to/filename>'\") to override.";
|
341 | } else if (isInNodeModules) {
|
342 | message = "File ignored by default. Use \"--ignore-pattern '!node_modules/*'\" to override.";
|
343 | } else if (isInBowerComponents) {
|
344 | message = "File ignored by default. Use \"--ignore-pattern '!bower_components/*'\" to override.";
|
345 | } else {
|
346 | message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override.";
|
347 | }
|
348 |
|
349 | return {
|
350 | filePath: path.resolve(filePath),
|
351 | messages: [
|
352 | {
|
353 | fatal: false,
|
354 | severity: 1,
|
355 | message
|
356 | }
|
357 | ],
|
358 | errorCount: 0,
|
359 | warningCount: 1,
|
360 | fixableErrorCount: 0,
|
361 | fixableWarningCount: 0
|
362 | };
|
363 | }
|
364 |
|
365 |
|
366 | /**
|
367 | * Checks if the given message is an error message.
|
368 | * @param {Object} message The message to check.
|
369 | * @returns {boolean} Whether or not the message is an error message.
|
370 | * @private
|
371 | */
|
372 | function isErrorMessage(message) {
|
373 | return message.severity === 2;
|
374 | }
|
375 |
|
376 |
|
377 | /**
|
378 | * return the cacheFile to be used by eslint, based on whether the provided parameter is
|
379 | * a directory or looks like a directory (ends in `path.sep`), in which case the file
|
380 | * name will be the `cacheFile/.cache_hashOfCWD`
|
381 | *
|
382 | * if cacheFile points to a file or looks like a file then in will just use that file
|
383 | *
|
384 | * @param {string} cacheFile The name of file to be used to store the cache
|
385 | * @param {string} cwd Current working directory
|
386 | * @returns {string} the resolved path to the cache file
|
387 | */
|
388 | function getCacheFile(cacheFile, cwd) {
|
389 |
|
390 | /*
|
391 | * make sure the path separators are normalized for the environment/os
|
392 | * keeping the trailing path separator if present
|
393 | */
|
394 | cacheFile = path.normalize(cacheFile);
|
395 |
|
396 | const resolvedCacheFile = path.resolve(cwd, cacheFile);
|
397 | const looksLikeADirectory = cacheFile[cacheFile.length - 1 ] === path.sep;
|
398 |
|
399 | /**
|
400 | * return the name for the cache file in case the provided parameter is a directory
|
401 | * @returns {string} the resolved path to the cacheFile
|
402 | */
|
403 | function getCacheFileForDirectory() {
|
404 | return path.join(resolvedCacheFile, `.cache_${hash(cwd)}`);
|
405 | }
|
406 |
|
407 | let fileStats;
|
408 |
|
409 | try {
|
410 | fileStats = fs.lstatSync(resolvedCacheFile);
|
411 | } catch (ex) {
|
412 | fileStats = null;
|
413 | }
|
414 |
|
415 |
|
416 | /*
|
417 | * in case the file exists we need to verify if the provided path
|
418 | * is a directory or a file. If it is a directory we want to create a file
|
419 | * inside that directory
|
420 | */
|
421 | if (fileStats) {
|
422 |
|
423 | /*
|
424 | * is a directory or is a file, but the original file the user provided
|
425 | * looks like a directory but `path.resolve` removed the `last path.sep`
|
426 | * so we need to still treat this like a directory
|
427 | */
|
428 | if (fileStats.isDirectory() || looksLikeADirectory) {
|
429 | return getCacheFileForDirectory();
|
430 | }
|
431 |
|
432 | // is file so just use that file
|
433 | return resolvedCacheFile;
|
434 | }
|
435 |
|
436 | /*
|
437 | * here we known the file or directory doesn't exist,
|
438 | * so we will try to infer if its a directory if it looks like a directory
|
439 | * for the current operating system.
|
440 | */
|
441 |
|
442 | // if the last character passed is a path separator we assume is a directory
|
443 | if (looksLikeADirectory) {
|
444 | return getCacheFileForDirectory();
|
445 | }
|
446 |
|
447 | return resolvedCacheFile;
|
448 | }
|
449 |
|
450 | //------------------------------------------------------------------------------
|
451 | // Public Interface
|
452 | //------------------------------------------------------------------------------
|
453 |
|
454 | class CLIEngine {
|
455 |
|
456 | /**
|
457 | * Creates a new instance of the core CLI engine.
|
458 | * @param {CLIEngineOptions} options The options for this instance.
|
459 | * @constructor
|
460 | */
|
461 | constructor(options) {
|
462 |
|
463 | options = Object.assign(
|
464 | Object.create(null),
|
465 | defaultOptions,
|
466 | { cwd: process.cwd() },
|
467 | options
|
468 | );
|
469 |
|
470 | /**
|
471 | * Stored options for this instance
|
472 | * @type {Object}
|
473 | */
|
474 | this.options = options;
|
475 | this.linter = new Linter();
|
476 |
|
477 | const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd);
|
478 |
|
479 | /**
|
480 | * Cache used to avoid operating on files that haven't changed since the
|
481 | * last successful execution (e.g., file passed linting with no errors and
|
482 | * no warnings).
|
483 | * @type {Object}
|
484 | */
|
485 | this._fileCache = fileEntryCache.create(cacheFile);
|
486 |
|
487 | // load in additional rules
|
488 | if (this.options.rulePaths) {
|
489 | const cwd = this.options.cwd;
|
490 |
|
491 | this.options.rulePaths.forEach(rulesdir => {
|
492 | debug(`Loading rules from ${rulesdir}`);
|
493 | this.linter.rules.load(rulesdir, cwd);
|
494 | });
|
495 | }
|
496 |
|
497 | Object.keys(this.options.rules || {}).forEach(name => {
|
498 | validator.validateRuleOptions(name, this.options.rules[name], "CLI", this.linter.rules);
|
499 | });
|
500 |
|
501 | this.config = new Config(this.options, this.linter);
|
502 | }
|
503 |
|
504 | /**
|
505 | * Returns results that only contains errors.
|
506 | * @param {LintResult[]} results The results to filter.
|
507 | * @returns {LintResult[]} The filtered results.
|
508 | */
|
509 | static getErrorResults(results) {
|
510 | const filtered = [];
|
511 |
|
512 | results.forEach(result => {
|
513 | const filteredMessages = result.messages.filter(isErrorMessage);
|
514 |
|
515 | if (filteredMessages.length > 0) {
|
516 | filtered.push(
|
517 | Object.assign(result, {
|
518 | messages: filteredMessages,
|
519 | errorCount: filteredMessages.length,
|
520 | warningCount: 0,
|
521 | fixableErrorCount: result.fixableErrorCount,
|
522 | fixableWarningCount: 0
|
523 | })
|
524 | );
|
525 | }
|
526 | });
|
527 |
|
528 | return filtered;
|
529 | }
|
530 |
|
531 | /**
|
532 | * Outputs fixes from the given results to files.
|
533 | * @param {Object} report The report object created by CLIEngine.
|
534 | * @returns {void}
|
535 | */
|
536 | static outputFixes(report) {
|
537 | report.results.filter(result => result.hasOwnProperty("output")).forEach(result => {
|
538 | fs.writeFileSync(result.filePath, result.output);
|
539 | });
|
540 | }
|
541 |
|
542 |
|
543 | /**
|
544 | * Add a plugin by passing its configuration
|
545 | * @param {string} name Name of the plugin.
|
546 | * @param {Object} pluginobject Plugin configuration object.
|
547 | * @returns {void}
|
548 | */
|
549 | addPlugin(name, pluginobject) {
|
550 | this.config.plugins.define(name, pluginobject);
|
551 | }
|
552 |
|
553 | /**
|
554 | * Resolves the patterns passed into executeOnFiles() into glob-based patterns
|
555 | * for easier handling.
|
556 | * @param {string[]} patterns The file patterns passed on the command line.
|
557 | * @returns {string[]} The equivalent glob patterns.
|
558 | */
|
559 | resolveFileGlobPatterns(patterns) {
|
560 | return globUtil.resolveFileGlobPatterns(patterns, this.options);
|
561 | }
|
562 |
|
563 | /**
|
564 | * Executes the current configuration on an array of file and directory names.
|
565 | * @param {string[]} patterns An array of file and directory names.
|
566 | * @returns {Object} The results for all files that were linted.
|
567 | */
|
568 | executeOnFiles(patterns) {
|
569 | const results = [],
|
570 | options = this.options,
|
571 | fileCache = this._fileCache,
|
572 | configHelper = this.config;
|
573 | let prevConfig; // the previous configuration used
|
574 |
|
575 | /**
|
576 | * Calculates the hash of the config file used to validate a given file
|
577 | * @param {string} filename The path of the file to retrieve a config object for to calculate the hash
|
578 | * @returns {string} the hash of the config
|
579 | */
|
580 | function hashOfConfigFor(filename) {
|
581 | const config = configHelper.getConfig(filename);
|
582 |
|
583 | if (!prevConfig) {
|
584 | prevConfig = {};
|
585 | }
|
586 |
|
587 | // reuse the previously hashed config if the config hasn't changed
|
588 | if (prevConfig.config !== config) {
|
589 |
|
590 | /*
|
591 | * config changed so we need to calculate the hash of the config
|
592 | * and the hash of the plugins being used
|
593 | */
|
594 | prevConfig.config = config;
|
595 |
|
596 | const eslintVersion = pkg.version;
|
597 |
|
598 | prevConfig.hash = hash(`${eslintVersion}_${stringify(config)}`);
|
599 | }
|
600 |
|
601 | return prevConfig.hash;
|
602 | }
|
603 |
|
604 | /**
|
605 | * Executes the linter on a file defined by the `filename`. Skips
|
606 | * unsupported file extensions and any files that are already linted.
|
607 | * @param {string} filename The resolved filename of the file to be linted
|
608 | * @param {boolean} warnIgnored always warn when a file is ignored
|
609 | * @param {Linter} linter Linter context
|
610 | * @returns {void}
|
611 | */
|
612 | function executeOnFile(filename, warnIgnored, linter) {
|
613 | let hashOfConfig,
|
614 | descriptor;
|
615 |
|
616 | if (warnIgnored) {
|
617 | results.push(createIgnoreResult(filename, options.cwd));
|
618 | return;
|
619 | }
|
620 |
|
621 | if (options.cache) {
|
622 |
|
623 | /*
|
624 | * get the descriptor for this file
|
625 | * with the metadata and the flag that determines if
|
626 | * the file has changed
|
627 | */
|
628 | descriptor = fileCache.getFileDescriptor(filename);
|
629 | const meta = descriptor.meta || {};
|
630 |
|
631 | hashOfConfig = hashOfConfigFor(filename);
|
632 |
|
633 | const changed = descriptor.changed || meta.hashOfConfig !== hashOfConfig;
|
634 |
|
635 | if (!changed) {
|
636 | debug(`Skipping file since hasn't changed: ${filename}`);
|
637 |
|
638 | /*
|
639 | * Add the the cached results (always will be 0 error and
|
640 | * 0 warnings). We should not cache results for files that
|
641 | * failed, in order to guarantee that next execution will
|
642 | * process those files as well.
|
643 | */
|
644 | results.push(descriptor.meta.results);
|
645 |
|
646 | // move to the next file
|
647 | return;
|
648 | }
|
649 | } else {
|
650 | fileCache.destroy();
|
651 | }
|
652 |
|
653 | debug(`Processing ${filename}`);
|
654 |
|
655 | const res = processFile(filename, configHelper, options, linter);
|
656 |
|
657 | if (options.cache) {
|
658 |
|
659 | /*
|
660 | * if a file contains errors or warnings we don't want to
|
661 | * store the file in the cache so we can guarantee that
|
662 | * next execution will also operate on this file
|
663 | */
|
664 | if (res.errorCount > 0 || res.warningCount > 0) {
|
665 | debug(`File has problems, skipping it: ${filename}`);
|
666 |
|
667 | // remove the entry from the cache
|
668 | fileCache.removeEntry(filename);
|
669 | } else {
|
670 |
|
671 | /*
|
672 | * since the file passed we store the result here
|
673 | * TODO: check this as we might not need to store the
|
674 | * successful runs as it will always should be 0 errors and
|
675 | * 0 warnings.
|
676 | */
|
677 | descriptor.meta.hashOfConfig = hashOfConfig;
|
678 | descriptor.meta.results = res;
|
679 | }
|
680 | }
|
681 |
|
682 | results.push(res);
|
683 | }
|
684 |
|
685 | const startTime = Date.now();
|
686 |
|
687 |
|
688 |
|
689 | patterns = this.resolveFileGlobPatterns(patterns);
|
690 | const fileList = globUtil.listFilesToProcess(patterns, options);
|
691 |
|
692 | fileList.forEach(fileInfo => {
|
693 | executeOnFile(fileInfo.filename, fileInfo.ignored, this.linter);
|
694 | });
|
695 |
|
696 | const stats = calculateStatsPerRun(results);
|
697 |
|
698 | if (options.cache) {
|
699 |
|
700 | // persist the cache to disk
|
701 | fileCache.reconcile();
|
702 | }
|
703 |
|
704 | debug(`Linting complete in: ${Date.now() - startTime}ms`);
|
705 |
|
706 | return {
|
707 | results,
|
708 | errorCount: stats.errorCount,
|
709 | warningCount: stats.warningCount,
|
710 | fixableErrorCount: stats.fixableErrorCount,
|
711 | fixableWarningCount: stats.fixableWarningCount
|
712 | };
|
713 | }
|
714 |
|
715 | /**
|
716 | * Executes the current configuration on text.
|
717 | * @param {string} text A string of JavaScript code to lint.
|
718 | * @param {string} filename An optional string representing the texts filename.
|
719 | * @param {boolean} warnIgnored Always warn when a file is ignored
|
720 | * @returns {Object} The results for the linting.
|
721 | */
|
722 | executeOnText(text, filename, warnIgnored) {
|
723 |
|
724 | const results = [],
|
725 | options = this.options,
|
726 | configHelper = this.config,
|
727 | ignoredPaths = new IgnoredPaths(options);
|
728 |
|
729 | // resolve filename based on options.cwd (for reporting, ignoredPaths also resolves)
|
730 | if (filename && !path.isAbsolute(filename)) {
|
731 | filename = path.resolve(options.cwd, filename);
|
732 | }
|
733 |
|
734 | if (filename && ignoredPaths.contains(filename)) {
|
735 | if (warnIgnored) {
|
736 | results.push(createIgnoreResult(filename, options.cwd));
|
737 | }
|
738 | } else {
|
739 | results.push(processText(text, configHelper, filename, options.fix, options.allowInlineConfig, this.linter));
|
740 | }
|
741 |
|
742 | const stats = calculateStatsPerRun(results);
|
743 |
|
744 | return {
|
745 | results,
|
746 | errorCount: stats.errorCount,
|
747 | warningCount: stats.warningCount,
|
748 | fixableErrorCount: stats.fixableErrorCount,
|
749 | fixableWarningCount: stats.fixableWarningCount
|
750 | };
|
751 | }
|
752 |
|
753 | /**
|
754 | * Returns a configuration object for the given file based on the CLI options.
|
755 | * This is the same logic used by the ESLint CLI executable to determine
|
756 | * configuration for each file it processes.
|
757 | * @param {string} filePath The path of the file to retrieve a config object for.
|
758 | * @returns {Object} A configuration object for the file.
|
759 | */
|
760 | getConfigForFile(filePath) {
|
761 | const configHelper = this.config;
|
762 |
|
763 | return configHelper.getConfig(filePath);
|
764 | }
|
765 |
|
766 | /**
|
767 | * Checks if a given path is ignored by ESLint.
|
768 | * @param {string} filePath The path of the file to check.
|
769 | * @returns {boolean} Whether or not the given path is ignored.
|
770 | */
|
771 | isPathIgnored(filePath) {
|
772 | const resolvedPath = path.resolve(this.options.cwd, filePath);
|
773 | const ignoredPaths = new IgnoredPaths(this.options);
|
774 |
|
775 | return ignoredPaths.contains(resolvedPath);
|
776 | }
|
777 |
|
778 | /**
|
779 | * Returns the formatter representing the given format or null if no formatter
|
780 | * with the given name can be found.
|
781 | * @param {string} [format] The name of the format to load or the path to a
|
782 | * custom formatter.
|
783 | * @returns {Function} The formatter function or null if not found.
|
784 | */
|
785 | getFormatter(format) {
|
786 |
|
787 | let formatterPath;
|
788 |
|
789 | // default is stylish
|
790 | format = format || "stylish";
|
791 |
|
792 | // only strings are valid formatters
|
793 | if (typeof format === "string") {
|
794 |
|
795 | // replace \ with / for Windows compatibility
|
796 | format = format.replace(/\\/g, "/");
|
797 |
|
798 | // if there's a slash, then it's a file
|
799 | if (format.indexOf("/") > -1) {
|
800 | const cwd = this.options ? this.options.cwd : process.cwd();
|
801 |
|
802 | formatterPath = path.resolve(cwd, format);
|
803 | } else {
|
804 | formatterPath = `./formatters/${format}`;
|
805 | }
|
806 |
|
807 | try {
|
808 | return require(formatterPath);
|
809 | } catch (ex) {
|
810 | ex.message = `There was a problem loading formatter: ${formatterPath}\nError: ${ex.message}`;
|
811 | throw ex;
|
812 | }
|
813 |
|
814 | } else {
|
815 | return null;
|
816 | }
|
817 | }
|
818 | }
|
819 |
|
820 | CLIEngine.version = pkg.version;
|
821 | CLIEngine.getFormatter = CLIEngine.prototype.getFormatter;
|
822 |
|
823 | module.exports = CLIEngine;
|