]> git.proxmox.com Git - pve-eslint.git/blame - eslint/lib/linter/linter.js
update to 7.1.0 sources
[pve-eslint.git] / eslint / lib / linter / linter.js
CommitLineData
eb39fafa
DC
1/**
2 * @fileoverview Main Linter Class
3 * @author Gyandeep Singh
4 * @author aladdin-add
5 */
6
7"use strict";
8
9//------------------------------------------------------------------------------
10// Requirements
11//------------------------------------------------------------------------------
12
13const
14 path = require("path"),
15 eslintScope = require("eslint-scope"),
16 evk = require("eslint-visitor-keys"),
17 espree = require("espree"),
18 lodash = require("lodash"),
19 BuiltInEnvironments = require("../../conf/environments"),
20 pkg = require("../../package.json"),
21 astUtils = require("../shared/ast-utils"),
22 ConfigOps = require("../shared/config-ops"),
23 validator = require("../shared/config-validator"),
24 Traverser = require("../shared/traverser"),
25 { SourceCode } = require("../source-code"),
26 CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"),
27 applyDisableDirectives = require("./apply-disable-directives"),
28 ConfigCommentParser = require("./config-comment-parser"),
29 NodeEventGenerator = require("./node-event-generator"),
30 createReportTranslator = require("./report-translator"),
31 Rules = require("./rules"),
32 createEmitter = require("./safe-emitter"),
33 SourceCodeFixer = require("./source-code-fixer"),
34 timing = require("./timing"),
35 ruleReplacements = require("../../conf/replacements.json");
36
37const debug = require("debug")("eslint:linter");
38const MAX_AUTOFIX_PASSES = 10;
39const DEFAULT_PARSER_NAME = "espree";
40const commentParser = new ConfigCommentParser();
41const DEFAULT_ERROR_LOC = { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } };
42
43//------------------------------------------------------------------------------
44// Typedefs
45//------------------------------------------------------------------------------
46
47/** @typedef {InstanceType<import("../cli-engine/config-array")["ConfigArray"]>} ConfigArray */
48/** @typedef {InstanceType<import("../cli-engine/config-array")["ExtractedConfig"]>} ExtractedConfig */
49/** @typedef {import("../shared/types").ConfigData} ConfigData */
50/** @typedef {import("../shared/types").Environment} Environment */
51/** @typedef {import("../shared/types").GlobalConf} GlobalConf */
52/** @typedef {import("../shared/types").LintMessage} LintMessage */
53/** @typedef {import("../shared/types").ParserOptions} ParserOptions */
54/** @typedef {import("../shared/types").Processor} Processor */
55/** @typedef {import("../shared/types").Rule} Rule */
56
57/**
58 * @template T
59 * @typedef {{ [P in keyof T]-?: T[P] }} Required
60 */
61
62/**
63 * @typedef {Object} DisableDirective
64 * @property {("disable"|"enable"|"disable-line"|"disable-next-line")} type
65 * @property {number} line
66 * @property {number} column
67 * @property {(string|null)} ruleId
68 */
69
70/**
71 * The private data for `Linter` instance.
72 * @typedef {Object} LinterInternalSlots
73 * @property {ConfigArray|null} lastConfigArray The `ConfigArray` instance that the last `verify()` call used.
74 * @property {SourceCode|null} lastSourceCode The `SourceCode` instance that the last `verify()` call used.
75 * @property {Map<string, Parser>} parserMap The loaded parsers.
76 * @property {Rules} ruleMap The loaded rules.
77 */
78
79/**
80 * @typedef {Object} VerifyOptions
81 * @property {boolean} [allowInlineConfig] Allow/disallow inline comments' ability
82 * to change config once it is set. Defaults to true if not supplied.
83 * Useful if you want to validate JS without comments overriding rules.
84 * @property {boolean} [disableFixes] if `true` then the linter doesn't make `fix`
85 * properties into the lint result.
86 * @property {string} [filename] the filename of the source code.
87 * @property {boolean | "off" | "warn" | "error"} [reportUnusedDisableDirectives] Adds reported errors for
88 * unused `eslint-disable` directives.
89 */
90
91/**
92 * @typedef {Object} ProcessorOptions
93 * @property {(filename:string, text:string) => boolean} [filterCodeBlock] the
94 * predicate function that selects adopt code blocks.
95 * @property {Processor["postprocess"]} [postprocess] postprocessor for report
96 * messages. If provided, this should accept an array of the message lists
97 * for each code block returned from the preprocessor, apply a mapping to
98 * the messages as appropriate, and return a one-dimensional array of
99 * messages.
100 * @property {Processor["preprocess"]} [preprocess] preprocessor for source text.
101 * If provided, this should accept a string of source text, and return an
102 * array of code blocks to lint.
103 */
104
105/**
106 * @typedef {Object} FixOptions
107 * @property {boolean | ((message: LintMessage) => boolean)} [fix] Determines
108 * whether fixes should be applied.
109 */
110
111/**
112 * @typedef {Object} InternalOptions
113 * @property {string | null} warnInlineConfig The config name what `noInlineConfig` setting came from. If `noInlineConfig` setting didn't exist, this is null. If this is a config name, then the linter warns directive comments.
114 * @property {"off" | "warn" | "error"} reportUnusedDisableDirectives (boolean values were normalized)
115 */
116
117//------------------------------------------------------------------------------
118// Helpers
119//------------------------------------------------------------------------------
120
121/**
122 * Ensures that variables representing built-in properties of the Global Object,
123 * and any globals declared by special block comments, are present in the global
124 * scope.
125 * @param {Scope} globalScope The global scope.
126 * @param {Object} configGlobals The globals declared in configuration
127 * @param {{exportedVariables: Object, enabledGlobals: Object}} commentDirectives Directives from comment configuration
128 * @returns {void}
129 */
130function addDeclaredGlobals(globalScope, configGlobals, { exportedVariables, enabledGlobals }) {
131
132 // Define configured global variables.
133 for (const id of new Set([...Object.keys(configGlobals), ...Object.keys(enabledGlobals)])) {
134
135 /*
136 * `ConfigOps.normalizeConfigGlobal` will throw an error if a configured global value is invalid. However, these errors would
137 * typically be caught when validating a config anyway (validity for inline global comments is checked separately).
138 */
139 const configValue = configGlobals[id] === void 0 ? void 0 : ConfigOps.normalizeConfigGlobal(configGlobals[id]);
140 const commentValue = enabledGlobals[id] && enabledGlobals[id].value;
141 const value = commentValue || configValue;
142 const sourceComments = enabledGlobals[id] && enabledGlobals[id].comments;
143
144 if (value === "off") {
145 continue;
146 }
147
148 let variable = globalScope.set.get(id);
149
150 if (!variable) {
151 variable = new eslintScope.Variable(id, globalScope);
152
153 globalScope.variables.push(variable);
154 globalScope.set.set(id, variable);
155 }
156
157 variable.eslintImplicitGlobalSetting = configValue;
158 variable.eslintExplicitGlobal = sourceComments !== void 0;
159 variable.eslintExplicitGlobalComments = sourceComments;
160 variable.writeable = (value === "writable");
161 }
162
163 // mark all exported variables as such
164 Object.keys(exportedVariables).forEach(name => {
165 const variable = globalScope.set.get(name);
166
167 if (variable) {
168 variable.eslintUsed = true;
169 }
170 });
171
172 /*
173 * "through" contains all references which definitions cannot be found.
174 * Since we augment the global scope using configuration, we need to update
175 * references and remove the ones that were added by configuration.
176 */
177 globalScope.through = globalScope.through.filter(reference => {
178 const name = reference.identifier.name;
179 const variable = globalScope.set.get(name);
180
181 if (variable) {
182
183 /*
184 * Links the variable and the reference.
185 * And this reference is removed from `Scope#through`.
186 */
187 reference.resolved = variable;
188 variable.references.push(reference);
189
190 return false;
191 }
192
193 return true;
194 });
195}
196
197/**
198 * creates a missing-rule message.
199 * @param {string} ruleId the ruleId to create
200 * @returns {string} created error message
201 * @private
202 */
203function createMissingRuleMessage(ruleId) {
204 return Object.prototype.hasOwnProperty.call(ruleReplacements.rules, ruleId)
205 ? `Rule '${ruleId}' was removed and replaced by: ${ruleReplacements.rules[ruleId].join(", ")}`
206 : `Definition for rule '${ruleId}' was not found.`;
207}
208
209/**
210 * creates a linting problem
211 * @param {Object} options to create linting error
212 * @param {string} [options.ruleId] the ruleId to report
213 * @param {Object} [options.loc] the loc to report
214 * @param {string} [options.message] the error message to report
215 * @param {string} [options.severity] the error message to report
216 * @returns {LintMessage} created problem, returns a missing-rule problem if only provided ruleId.
217 * @private
218 */
219function createLintingProblem(options) {
220 const {
221 ruleId = null,
222 loc = DEFAULT_ERROR_LOC,
223 message = createMissingRuleMessage(options.ruleId),
224 severity = 2
225 } = options;
226
227 return {
228 ruleId,
229 message,
230 line: loc.start.line,
231 column: loc.start.column + 1,
232 endLine: loc.end.line,
233 endColumn: loc.end.column + 1,
234 severity,
235 nodeType: null
236 };
237}
238
239/**
240 * Creates a collection of disable directives from a comment
241 * @param {Object} options to create disable directives
242 * @param {("disable"|"enable"|"disable-line"|"disable-next-line")} options.type The type of directive comment
243 * @param {{line: number, column: number}} options.loc The 0-based location of the comment token
244 * @param {string} options.value The value after the directive in the comment
245 * comment specified no specific rules, so it applies to all rules (e.g. `eslint-disable`)
246 * @param {function(string): {create: Function}} options.ruleMapper A map from rule IDs to defined rules
247 * @returns {Object} Directives and problems from the comment
248 */
249function createDisableDirectives(options) {
250 const { type, loc, value, ruleMapper } = options;
251 const ruleIds = Object.keys(commentParser.parseListConfig(value));
252 const directiveRules = ruleIds.length ? ruleIds : [null];
253 const result = {
254 directives: [], // valid disable directives
255 directiveProblems: [] // problems in directives
256 };
257
258 for (const ruleId of directiveRules) {
259
260 // push to directives, if the rule is defined(including null, e.g. /*eslint enable*/)
261 if (ruleId === null || ruleMapper(ruleId) !== null) {
262 result.directives.push({ type, line: loc.start.line, column: loc.start.column + 1, ruleId });
263 } else {
264 result.directiveProblems.push(createLintingProblem({ ruleId, loc }));
265 }
266 }
267 return result;
268}
269
270/**
271 * Remove the ignored part from a given directive comment and trim it.
272 * @param {string} value The comment text to strip.
273 * @returns {string} The stripped text.
274 */
275function stripDirectiveComment(value) {
276 return value.split(/\s-{2,}\s/u)[0].trim();
277}
278
279/**
280 * Parses comments in file to extract file-specific config of rules, globals
281 * and environments and merges them with global config; also code blocks
282 * where reporting is disabled or enabled and merges them with reporting config.
283 * @param {string} filename The file being checked.
284 * @param {ASTNode} ast The top node of the AST.
285 * @param {function(string): {create: Function}} ruleMapper A map from rule IDs to defined rules
286 * @param {string|null} warnInlineConfig If a string then it should warn directive comments as disabled. The string value is the config name what the setting came from.
287 * @returns {{configuredRules: Object, enabledGlobals: {value:string,comment:Token}[], exportedVariables: Object, problems: Problem[], disableDirectives: DisableDirective[]}}
288 * A collection of the directive comments that were found, along with any problems that occurred when parsing
289 */
290function getDirectiveComments(filename, ast, ruleMapper, warnInlineConfig) {
291 const configuredRules = {};
292 const enabledGlobals = Object.create(null);
293 const exportedVariables = {};
294 const problems = [];
295 const disableDirectives = [];
296
297 ast.comments.filter(token => token.type !== "Shebang").forEach(comment => {
298 const trimmedCommentText = stripDirectiveComment(comment.value);
299 const match = /^(eslint(?:-env|-enable|-disable(?:(?:-next)?-line)?)?|exported|globals?)(?:\s|$)/u.exec(trimmedCommentText);
300
301 if (!match) {
302 return;
303 }
304 const directiveText = match[1];
305 const lineCommentSupported = /^eslint-disable-(next-)?line$/u.test(directiveText);
306
307 if (comment.type === "Line" && !lineCommentSupported) {
308 return;
309 }
310
311 if (warnInlineConfig) {
312 const kind = comment.type === "Block" ? `/*${directiveText}*/` : `//${directiveText}`;
313
314 problems.push(createLintingProblem({
315 ruleId: null,
316 message: `'${kind}' has no effect because you have 'noInlineConfig' setting in ${warnInlineConfig}.`,
317 loc: comment.loc,
318 severity: 1
319 }));
320 return;
321 }
322
323 if (lineCommentSupported && comment.loc.start.line !== comment.loc.end.line) {
324 const message = `${directiveText} comment should not span multiple lines.`;
325
326 problems.push(createLintingProblem({
327 ruleId: null,
328 message,
329 loc: comment.loc
330 }));
331 return;
332 }
333
334 const directiveValue = trimmedCommentText.slice(match.index + directiveText.length);
335
336 switch (directiveText) {
337 case "eslint-disable":
338 case "eslint-enable":
339 case "eslint-disable-next-line":
340 case "eslint-disable-line": {
341 const directiveType = directiveText.slice("eslint-".length);
342 const options = { type: directiveType, loc: comment.loc, value: directiveValue, ruleMapper };
343 const { directives, directiveProblems } = createDisableDirectives(options);
344
345 disableDirectives.push(...directives);
346 problems.push(...directiveProblems);
347 break;
348 }
349
350 case "exported":
351 Object.assign(exportedVariables, commentParser.parseStringConfig(directiveValue, comment));
352 break;
353
354 case "globals":
355 case "global":
356 for (const [id, { value }] of Object.entries(commentParser.parseStringConfig(directiveValue, comment))) {
357 let normalizedValue;
358
359 try {
360 normalizedValue = ConfigOps.normalizeConfigGlobal(value);
361 } catch (err) {
362 problems.push(createLintingProblem({
363 ruleId: null,
364 loc: comment.loc,
365 message: err.message
366 }));
367 continue;
368 }
369
370 if (enabledGlobals[id]) {
371 enabledGlobals[id].comments.push(comment);
372 enabledGlobals[id].value = normalizedValue;
373 } else {
374 enabledGlobals[id] = {
375 comments: [comment],
376 value: normalizedValue
377 };
378 }
379 }
380 break;
381
382 case "eslint": {
383 const parseResult = commentParser.parseJsonConfig(directiveValue, comment.loc);
384
385 if (parseResult.success) {
386 Object.keys(parseResult.config).forEach(name => {
387 const rule = ruleMapper(name);
388 const ruleValue = parseResult.config[name];
389
390 if (rule === null) {
391 problems.push(createLintingProblem({ ruleId: name, loc: comment.loc }));
392 return;
393 }
394
395 try {
396 validator.validateRuleOptions(rule, name, ruleValue);
397 } catch (err) {
398 problems.push(createLintingProblem({
399 ruleId: name,
400 message: err.message,
401 loc: comment.loc
402 }));
403
404 // do not apply the config, if found invalid options.
405 return;
406 }
407
408 configuredRules[name] = ruleValue;
409 });
410 } else {
411 problems.push(parseResult.error);
412 }
413
414 break;
415 }
416
417 // no default
418 }
419 });
420
421 return {
422 configuredRules,
423 enabledGlobals,
424 exportedVariables,
425 problems,
426 disableDirectives
427 };
428}
429
430/**
431 * Normalize ECMAScript version from the initial config
432 * @param {number} ecmaVersion ECMAScript version from the initial config
433 * @returns {number} normalized ECMAScript version
434 */
435function normalizeEcmaVersion(ecmaVersion) {
436
437 /*
438 * Calculate ECMAScript edition number from official year version starting with
439 * ES2015, which corresponds with ES6 (or a difference of 2009).
440 */
441 return ecmaVersion >= 2015 ? ecmaVersion - 2009 : ecmaVersion;
442}
443
444const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)\*\//gu;
445
446/**
447 * Checks whether or not there is a comment which has "eslint-env *" in a given text.
448 * @param {string} text A source code text to check.
449 * @returns {Object|null} A result of parseListConfig() with "eslint-env *" comment.
450 */
451function findEslintEnv(text) {
452 let match, retv;
453
454 eslintEnvPattern.lastIndex = 0;
455
456 while ((match = eslintEnvPattern.exec(text)) !== null) {
457 retv = Object.assign(
458 retv || {},
459 commentParser.parseListConfig(stripDirectiveComment(match[1]))
460 );
461 }
462
463 return retv;
464}
465
466/**
467 * Convert "/path/to/<text>" to "<text>".
468 * `CLIEngine#executeOnText()` method gives "/path/to/<text>" if the filename
469 * was omitted because `configArray.extractConfig()` requires an absolute path.
470 * But the linter should pass `<text>` to `RuleContext#getFilename()` in that
471 * case.
472 * Also, code blocks can have their virtual filename. If the parent filename was
473 * `<text>`, the virtual filename is `<text>/0_foo.js` or something like (i.e.,
474 * it's not an absolute path).
475 * @param {string} filename The filename to normalize.
476 * @returns {string} The normalized filename.
477 */
478function normalizeFilename(filename) {
479 const parts = filename.split(path.sep);
480 const index = parts.lastIndexOf("<text>");
481
482 return index === -1 ? filename : parts.slice(index).join(path.sep);
483}
484
485/**
486 * Normalizes the possible options for `linter.verify` and `linter.verifyAndFix` to a
487 * consistent shape.
488 * @param {VerifyOptions} providedOptions Options
489 * @param {ConfigData} config Config.
490 * @returns {Required<VerifyOptions> & InternalOptions} Normalized options
491 */
492function normalizeVerifyOptions(providedOptions, config) {
493 const disableInlineConfig = config.noInlineConfig === true;
494 const ignoreInlineConfig = providedOptions.allowInlineConfig === false;
495 const configNameOfNoInlineConfig = config.configNameOfNoInlineConfig
496 ? ` (${config.configNameOfNoInlineConfig})`
497 : "";
498
499 let reportUnusedDisableDirectives = providedOptions.reportUnusedDisableDirectives;
500
501 if (typeof reportUnusedDisableDirectives === "boolean") {
502 reportUnusedDisableDirectives = reportUnusedDisableDirectives ? "error" : "off";
503 }
504 if (typeof reportUnusedDisableDirectives !== "string") {
505 reportUnusedDisableDirectives = config.reportUnusedDisableDirectives ? "warn" : "off";
506 }
507
508 return {
509 filename: normalizeFilename(providedOptions.filename || "<input>"),
510 allowInlineConfig: !ignoreInlineConfig,
511 warnInlineConfig: disableInlineConfig && !ignoreInlineConfig
512 ? `your config${configNameOfNoInlineConfig}`
513 : null,
514 reportUnusedDisableDirectives,
515 disableFixes: Boolean(providedOptions.disableFixes)
516 };
517}
518
519/**
520 * Combines the provided parserOptions with the options from environments
521 * @param {string} parserName The parser name which uses this options.
522 * @param {ParserOptions} providedOptions The provided 'parserOptions' key in a config
523 * @param {Environment[]} enabledEnvironments The environments enabled in configuration and with inline comments
524 * @returns {ParserOptions} Resulting parser options after merge
525 */
526function resolveParserOptions(parserName, providedOptions, enabledEnvironments) {
527 const parserOptionsFromEnv = enabledEnvironments
528 .filter(env => env.parserOptions)
529 .reduce((parserOptions, env) => lodash.merge(parserOptions, env.parserOptions), {});
530 const mergedParserOptions = lodash.merge(parserOptionsFromEnv, providedOptions || {});
531 const isModule = mergedParserOptions.sourceType === "module";
532
533 if (isModule) {
534
535 /*
536 * can't have global return inside of modules
537 * TODO: espree validate parserOptions.globalReturn when sourceType is setting to module.(@aladdin-add)
538 */
539 mergedParserOptions.ecmaFeatures = Object.assign({}, mergedParserOptions.ecmaFeatures, { globalReturn: false });
540 }
541
542 /*
543 * TODO: @aladdin-add
544 * 1. for a 3rd-party parser, do not normalize parserOptions
545 * 2. for espree, no need to do this (espree will do it)
546 */
547 mergedParserOptions.ecmaVersion = normalizeEcmaVersion(mergedParserOptions.ecmaVersion);
548
549 return mergedParserOptions;
550}
551
552/**
553 * Combines the provided globals object with the globals from environments
554 * @param {Record<string, GlobalConf>} providedGlobals The 'globals' key in a config
555 * @param {Environment[]} enabledEnvironments The environments enabled in configuration and with inline comments
556 * @returns {Record<string, GlobalConf>} The resolved globals object
557 */
558function resolveGlobals(providedGlobals, enabledEnvironments) {
559 return Object.assign(
560 {},
561 ...enabledEnvironments.filter(env => env.globals).map(env => env.globals),
562 providedGlobals
563 );
564}
565
566/**
567 * Strips Unicode BOM from a given text.
568 * @param {string} text A text to strip.
569 * @returns {string} The stripped text.
570 */
571function stripUnicodeBOM(text) {
572
573 /*
574 * Check Unicode BOM.
575 * In JavaScript, string data is stored as UTF-16, so BOM is 0xFEFF.
576 * http://www.ecma-international.org/ecma-262/6.0/#sec-unicode-format-control-characters
577 */
578 if (text.charCodeAt(0) === 0xFEFF) {
579 return text.slice(1);
580 }
581 return text;
582}
583
584/**
585 * Get the options for a rule (not including severity), if any
586 * @param {Array|number} ruleConfig rule configuration
587 * @returns {Array} of rule options, empty Array if none
588 */
589function getRuleOptions(ruleConfig) {
590 if (Array.isArray(ruleConfig)) {
591 return ruleConfig.slice(1);
592 }
593 return [];
594
595}
596
597/**
598 * Analyze scope of the given AST.
599 * @param {ASTNode} ast The `Program` node to analyze.
600 * @param {ParserOptions} parserOptions The parser options.
601 * @param {Record<string, string[]>} visitorKeys The visitor keys.
602 * @returns {ScopeManager} The analysis result.
603 */
604function analyzeScope(ast, parserOptions, visitorKeys) {
605 const ecmaFeatures = parserOptions.ecmaFeatures || {};
606 const ecmaVersion = parserOptions.ecmaVersion || 5;
607
608 return eslintScope.analyze(ast, {
609 ignoreEval: true,
610 nodejsScope: ecmaFeatures.globalReturn,
611 impliedStrict: ecmaFeatures.impliedStrict,
612 ecmaVersion,
613 sourceType: parserOptions.sourceType || "script",
614 childVisitorKeys: visitorKeys || evk.KEYS,
615 fallback: Traverser.getKeys
616 });
617}
618
619/**
620 * Parses text into an AST. Moved out here because the try-catch prevents
621 * optimization of functions, so it's best to keep the try-catch as isolated
622 * as possible
623 * @param {string} text The text to parse.
624 * @param {Parser} parser The parser to parse.
625 * @param {ParserOptions} providedParserOptions Options to pass to the parser
626 * @param {string} filePath The path to the file being parsed.
627 * @returns {{success: false, error: Problem}|{success: true, sourceCode: SourceCode}}
628 * An object containing the AST and parser services if parsing was successful, or the error if parsing failed
629 * @private
630 */
631function parse(text, parser, providedParserOptions, filePath) {
632 const textToParse = stripUnicodeBOM(text).replace(astUtils.shebangPattern, (match, captured) => `//${captured}`);
633 const parserOptions = Object.assign({}, providedParserOptions, {
634 loc: true,
635 range: true,
636 raw: true,
637 tokens: true,
638 comment: true,
639 eslintVisitorKeys: true,
640 eslintScopeManager: true,
641 filePath
642 });
643
644 /*
645 * Check for parsing errors first. If there's a parsing error, nothing
646 * else can happen. However, a parsing error does not throw an error
647 * from this method - it's just considered a fatal error message, a
648 * problem that ESLint identified just like any other.
649 */
650 try {
651 const parseResult = (typeof parser.parseForESLint === "function")
652 ? parser.parseForESLint(textToParse, parserOptions)
653 : { ast: parser.parse(textToParse, parserOptions) };
654 const ast = parseResult.ast;
655 const parserServices = parseResult.services || {};
656 const visitorKeys = parseResult.visitorKeys || evk.KEYS;
657 const scopeManager = parseResult.scopeManager || analyzeScope(ast, parserOptions, visitorKeys);
658
659 return {
660 success: true,
661
662 /*
663 * Save all values that `parseForESLint()` returned.
664 * If a `SourceCode` object is given as the first parameter instead of source code text,
665 * linter skips the parsing process and reuses the source code object.
666 * In that case, linter needs all the values that `parseForESLint()` returned.
667 */
668 sourceCode: new SourceCode({
669 text,
670 ast,
671 parserServices,
672 scopeManager,
673 visitorKeys
674 })
675 };
676 } catch (ex) {
677
678 // If the message includes a leading line number, strip it:
679 const message = `Parsing error: ${ex.message.replace(/^line \d+:/iu, "").trim()}`;
680
681 debug("%s\n%s", message, ex.stack);
682
683 return {
684 success: false,
685 error: {
686 ruleId: null,
687 fatal: true,
688 severity: 2,
689 message,
690 line: ex.lineNumber,
691 column: ex.column
692 }
693 };
694 }
695}
696
697/**
698 * Gets the scope for the current node
699 * @param {ScopeManager} scopeManager The scope manager for this AST
700 * @param {ASTNode} currentNode The node to get the scope of
701 * @returns {eslint-scope.Scope} The scope information for this node
702 */
703function getScope(scopeManager, currentNode) {
704
705 // On Program node, get the outermost scope to avoid return Node.js special function scope or ES modules scope.
706 const inner = currentNode.type !== "Program";
707
708 for (let node = currentNode; node; node = node.parent) {
709 const scope = scopeManager.acquire(node, inner);
710
711 if (scope) {
712 if (scope.type === "function-expression-name") {
713 return scope.childScopes[0];
714 }
715 return scope;
716 }
717 }
718
719 return scopeManager.scopes[0];
720}
721
722/**
723 * Marks a variable as used in the current scope
724 * @param {ScopeManager} scopeManager The scope manager for this AST. The scope may be mutated by this function.
725 * @param {ASTNode} currentNode The node currently being traversed
726 * @param {Object} parserOptions The options used to parse this text
727 * @param {string} name The name of the variable that should be marked as used.
728 * @returns {boolean} True if the variable was found and marked as used, false if not.
729 */
730function markVariableAsUsed(scopeManager, currentNode, parserOptions, name) {
731 const hasGlobalReturn = parserOptions.ecmaFeatures && parserOptions.ecmaFeatures.globalReturn;
732 const specialScope = hasGlobalReturn || parserOptions.sourceType === "module";
733 const currentScope = getScope(scopeManager, currentNode);
734
735 // Special Node.js scope means we need to start one level deeper
736 const initialScope = currentScope.type === "global" && specialScope ? currentScope.childScopes[0] : currentScope;
737
738 for (let scope = initialScope; scope; scope = scope.upper) {
739 const variable = scope.variables.find(scopeVar => scopeVar.name === name);
740
741 if (variable) {
742 variable.eslintUsed = true;
743 return true;
744 }
745 }
746
747 return false;
748}
749
750/**
751 * Runs a rule, and gets its listeners
752 * @param {Rule} rule A normalized rule with a `create` method
753 * @param {Context} ruleContext The context that should be passed to the rule
754 * @returns {Object} A map of selector listeners provided by the rule
755 */
756function createRuleListeners(rule, ruleContext) {
757 try {
758 return rule.create(ruleContext);
759 } catch (ex) {
760 ex.message = `Error while loading rule '${ruleContext.id}': ${ex.message}`;
761 throw ex;
762 }
763}
764
765/**
766 * Gets all the ancestors of a given node
767 * @param {ASTNode} node The node
768 * @returns {ASTNode[]} All the ancestor nodes in the AST, not including the provided node, starting
769 * from the root node and going inwards to the parent node.
770 */
771function getAncestors(node) {
772 const ancestorsStartingAtParent = [];
773
774 for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
775 ancestorsStartingAtParent.push(ancestor);
776 }
777
778 return ancestorsStartingAtParent.reverse();
779}
780
781// methods that exist on SourceCode object
782const DEPRECATED_SOURCECODE_PASSTHROUGHS = {
783 getSource: "getText",
784 getSourceLines: "getLines",
785 getAllComments: "getAllComments",
786 getNodeByRangeIndex: "getNodeByRangeIndex",
787 getComments: "getComments",
788 getCommentsBefore: "getCommentsBefore",
789 getCommentsAfter: "getCommentsAfter",
790 getCommentsInside: "getCommentsInside",
791 getJSDocComment: "getJSDocComment",
792 getFirstToken: "getFirstToken",
793 getFirstTokens: "getFirstTokens",
794 getLastToken: "getLastToken",
795 getLastTokens: "getLastTokens",
796 getTokenAfter: "getTokenAfter",
797 getTokenBefore: "getTokenBefore",
798 getTokenByRangeStart: "getTokenByRangeStart",
799 getTokens: "getTokens",
800 getTokensAfter: "getTokensAfter",
801 getTokensBefore: "getTokensBefore",
802 getTokensBetween: "getTokensBetween"
803};
804
805const BASE_TRAVERSAL_CONTEXT = Object.freeze(
806 Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).reduce(
807 (contextInfo, methodName) =>
808 Object.assign(contextInfo, {
809 [methodName](...args) {
810 return this.getSourceCode()[DEPRECATED_SOURCECODE_PASSTHROUGHS[methodName]](...args);
811 }
812 }),
813 {}
814 )
815);
816
817/**
818 * Runs the given rules on the given SourceCode object
819 * @param {SourceCode} sourceCode A SourceCode object for the given text
820 * @param {Object} configuredRules The rules configuration
821 * @param {function(string): Rule} ruleMapper A mapper function from rule names to rules
822 * @param {Object} parserOptions The options that were passed to the parser
823 * @param {string} parserName The name of the parser in the config
824 * @param {Object} settings The settings that were enabled in the config
825 * @param {string} filename The reported filename of the code
826 * @param {boolean} disableFixes If true, it doesn't make `fix` properties.
827 * @param {string | undefined} cwd cwd of the cli
828 * @returns {Problem[]} An array of reported problems
829 */
830function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename, disableFixes, cwd) {
831 const emitter = createEmitter();
832 const nodeQueue = [];
833 let currentNode = sourceCode.ast;
834
835 Traverser.traverse(sourceCode.ast, {
836 enter(node, parent) {
837 node.parent = parent;
838 nodeQueue.push({ isEntering: true, node });
839 },
840 leave(node) {
841 nodeQueue.push({ isEntering: false, node });
842 },
843 visitorKeys: sourceCode.visitorKeys
844 });
845
846 /*
847 * Create a frozen object with the ruleContext properties and methods that are shared by all rules.
848 * All rule contexts will inherit from this object. This avoids the performance penalty of copying all the
849 * properties once for each rule.
850 */
851 const sharedTraversalContext = Object.freeze(
852 Object.assign(
853 Object.create(BASE_TRAVERSAL_CONTEXT),
854 {
855 getAncestors: () => getAncestors(currentNode),
856 getDeclaredVariables: sourceCode.scopeManager.getDeclaredVariables.bind(sourceCode.scopeManager),
857 getCwd: () => cwd,
858 getFilename: () => filename,
859 getScope: () => getScope(sourceCode.scopeManager, currentNode),
860 getSourceCode: () => sourceCode,
861 markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, parserOptions, name),
862 parserOptions,
863 parserPath: parserName,
864 parserServices: sourceCode.parserServices,
865 settings
866 }
867 )
868 );
869
870
871 const lintingProblems = [];
872
873 Object.keys(configuredRules).forEach(ruleId => {
874 const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]);
875
876 // not load disabled rules
877 if (severity === 0) {
878 return;
879 }
880
881 const rule = ruleMapper(ruleId);
882
883 if (rule === null) {
884 lintingProblems.push(createLintingProblem({ ruleId }));
885 return;
886 }
887
888 const messageIds = rule.meta && rule.meta.messages;
889 let reportTranslator = null;
890 const ruleContext = Object.freeze(
891 Object.assign(
892 Object.create(sharedTraversalContext),
893 {
894 id: ruleId,
895 options: getRuleOptions(configuredRules[ruleId]),
896 report(...args) {
897
898 /*
899 * Create a report translator lazily.
900 * In a vast majority of cases, any given rule reports zero errors on a given
901 * piece of code. Creating a translator lazily avoids the performance cost of
902 * creating a new translator function for each rule that usually doesn't get
903 * called.
904 *
905 * Using lazy report translators improves end-to-end performance by about 3%
906 * with Node 8.4.0.
907 */
908 if (reportTranslator === null) {
909 reportTranslator = createReportTranslator({
910 ruleId,
911 severity,
912 sourceCode,
913 messageIds,
914 disableFixes
915 });
916 }
917 const problem = reportTranslator(...args);
918
919 if (problem.fix && rule.meta && !rule.meta.fixable) {
920 throw new Error("Fixable rules should export a `meta.fixable` property.");
921 }
922 lintingProblems.push(problem);
923 }
924 }
925 )
926 );
927
928 const ruleListeners = createRuleListeners(rule, ruleContext);
929
930 // add all the selectors from the rule as listeners
931 Object.keys(ruleListeners).forEach(selector => {
932 emitter.on(
933 selector,
934 timing.enabled
935 ? timing.time(ruleId, ruleListeners[selector])
936 : ruleListeners[selector]
937 );
938 });
939 });
940
ebb53d86
TL
941 // only run code path analyzer if the top level node is "Program", skip otherwise
942 const eventGenerator = nodeQueue[0].node.type === "Program" ? new CodePathAnalyzer(new NodeEventGenerator(emitter)) : new NodeEventGenerator(emitter);
eb39fafa
DC
943
944 nodeQueue.forEach(traversalInfo => {
945 currentNode = traversalInfo.node;
946
947 try {
948 if (traversalInfo.isEntering) {
949 eventGenerator.enterNode(currentNode);
950 } else {
951 eventGenerator.leaveNode(currentNode);
952 }
953 } catch (err) {
954 err.currentNode = currentNode;
955 throw err;
956 }
957 });
958
959 return lintingProblems;
960}
961
962/**
963 * Ensure the source code to be a string.
964 * @param {string|SourceCode} textOrSourceCode The text or source code object.
965 * @returns {string} The source code text.
966 */
967function ensureText(textOrSourceCode) {
968 if (typeof textOrSourceCode === "object") {
969 const { hasBOM, text } = textOrSourceCode;
970 const bom = hasBOM ? "\uFEFF" : "";
971
972 return bom + text;
973 }
974
975 return String(textOrSourceCode);
976}
977
978/**
979 * Get an environment.
980 * @param {LinterInternalSlots} slots The internal slots of Linter.
981 * @param {string} envId The environment ID to get.
982 * @returns {Environment|null} The environment.
983 */
984function getEnv(slots, envId) {
985 return (
986 (slots.lastConfigArray && slots.lastConfigArray.pluginEnvironments.get(envId)) ||
987 BuiltInEnvironments.get(envId) ||
988 null
989 );
990}
991
992/**
993 * Get a rule.
994 * @param {LinterInternalSlots} slots The internal slots of Linter.
995 * @param {string} ruleId The rule ID to get.
996 * @returns {Rule|null} The rule.
997 */
998function getRule(slots, ruleId) {
999 return (
1000 (slots.lastConfigArray && slots.lastConfigArray.pluginRules.get(ruleId)) ||
1001 slots.ruleMap.get(ruleId)
1002 );
1003}
1004
1005/**
1006 * Normalize the value of the cwd
1007 * @param {string | undefined} cwd raw value of the cwd, path to a directory that should be considered as the current working directory, can be undefined.
1008 * @returns {string | undefined} normalized cwd
1009 */
1010function normalizeCwd(cwd) {
1011 if (cwd) {
1012 return cwd;
1013 }
1014 if (typeof process === "object") {
1015 return process.cwd();
1016 }
1017
1018 // It's more explicit to assign the undefined
1019 // eslint-disable-next-line no-undefined
1020 return undefined;
1021}
1022
1023/**
1024 * The map to store private data.
1025 * @type {WeakMap<Linter, LinterInternalSlots>}
1026 */
1027const internalSlotsMap = new WeakMap();
1028
1029//------------------------------------------------------------------------------
1030// Public Interface
1031//------------------------------------------------------------------------------
1032
1033/**
1034 * Object that is responsible for verifying JavaScript text
1035 * @name eslint
1036 */
1037class Linter {
1038
1039 /**
1040 * Initialize the Linter.
1041 * @param {Object} [config] the config object
1042 * @param {string} [config.cwd] path to a directory that should be considered as the current working directory, can be undefined.
1043 */
1044 constructor({ cwd } = {}) {
1045 internalSlotsMap.set(this, {
1046 cwd: normalizeCwd(cwd),
1047 lastConfigArray: null,
1048 lastSourceCode: null,
1049 parserMap: new Map([["espree", espree]]),
1050 ruleMap: new Rules()
1051 });
1052
1053 this.version = pkg.version;
1054 }
1055
1056 /**
1057 * Getter for package version.
1058 * @static
1059 * @returns {string} The version from package.json.
1060 */
1061 static get version() {
1062 return pkg.version;
1063 }
1064
1065 /**
1066 * Same as linter.verify, except without support for processors.
1067 * @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object.
1068 * @param {ConfigData} providedConfig An ESLintConfig instance to configure everything.
1069 * @param {VerifyOptions} [providedOptions] The optional filename of the file being checked.
1070 * @returns {LintMessage[]} The results as an array of messages or an empty array if no messages.
1071 */
1072 _verifyWithoutProcessors(textOrSourceCode, providedConfig, providedOptions) {
1073 const slots = internalSlotsMap.get(this);
1074 const config = providedConfig || {};
1075 const options = normalizeVerifyOptions(providedOptions, config);
1076 let text;
1077
1078 // evaluate arguments
1079 if (typeof textOrSourceCode === "string") {
1080 slots.lastSourceCode = null;
1081 text = textOrSourceCode;
1082 } else {
1083 slots.lastSourceCode = textOrSourceCode;
1084 text = textOrSourceCode.text;
1085 }
1086
1087 // Resolve parser.
1088 let parserName = DEFAULT_PARSER_NAME;
1089 let parser = espree;
1090
1091 if (typeof config.parser === "object" && config.parser !== null) {
1092 parserName = config.parser.filePath;
1093 parser = config.parser.definition;
1094 } else if (typeof config.parser === "string") {
1095 if (!slots.parserMap.has(config.parser)) {
1096 return [{
1097 ruleId: null,
1098 fatal: true,
1099 severity: 2,
1100 message: `Configured parser '${config.parser}' was not found.`,
1101 line: 0,
1102 column: 0
1103 }];
1104 }
1105 parserName = config.parser;
1106 parser = slots.parserMap.get(config.parser);
1107 }
1108
1109 // search and apply "eslint-env *".
1110 const envInFile = options.allowInlineConfig && !options.warnInlineConfig
1111 ? findEslintEnv(text)
1112 : {};
1113 const resolvedEnvConfig = Object.assign({ builtin: true }, config.env, envInFile);
1114 const enabledEnvs = Object.keys(resolvedEnvConfig)
1115 .filter(envName => resolvedEnvConfig[envName])
1116 .map(envName => getEnv(slots, envName))
1117 .filter(env => env);
1118
1119 const parserOptions = resolveParserOptions(parserName, config.parserOptions || {}, enabledEnvs);
1120 const configuredGlobals = resolveGlobals(config.globals || {}, enabledEnvs);
1121 const settings = config.settings || {};
1122
1123 if (!slots.lastSourceCode) {
1124 const parseResult = parse(
1125 text,
1126 parser,
1127 parserOptions,
1128 options.filename
1129 );
1130
1131 if (!parseResult.success) {
1132 return [parseResult.error];
1133 }
1134
1135 slots.lastSourceCode = parseResult.sourceCode;
1136 } else {
1137
1138 /*
1139 * If the given source code object as the first argument does not have scopeManager, analyze the scope.
1140 * This is for backward compatibility (SourceCode is frozen so it cannot rebind).
1141 */
1142 if (!slots.lastSourceCode.scopeManager) {
1143 slots.lastSourceCode = new SourceCode({
1144 text: slots.lastSourceCode.text,
1145 ast: slots.lastSourceCode.ast,
1146 parserServices: slots.lastSourceCode.parserServices,
1147 visitorKeys: slots.lastSourceCode.visitorKeys,
1148 scopeManager: analyzeScope(slots.lastSourceCode.ast, parserOptions)
1149 });
1150 }
1151 }
1152
1153 const sourceCode = slots.lastSourceCode;
1154 const commentDirectives = options.allowInlineConfig
1155 ? getDirectiveComments(options.filename, sourceCode.ast, ruleId => getRule(slots, ruleId), options.warnInlineConfig)
1156 : { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] };
1157
1158 // augment global scope with declared global variables
1159 addDeclaredGlobals(
1160 sourceCode.scopeManager.scopes[0],
1161 configuredGlobals,
1162 { exportedVariables: commentDirectives.exportedVariables, enabledGlobals: commentDirectives.enabledGlobals }
1163 );
1164
1165 const configuredRules = Object.assign({}, config.rules, commentDirectives.configuredRules);
1166
1167 let lintingProblems;
1168
1169 try {
1170 lintingProblems = runRules(
1171 sourceCode,
1172 configuredRules,
1173 ruleId => getRule(slots, ruleId),
1174 parserOptions,
1175 parserName,
1176 settings,
1177 options.filename,
1178 options.disableFixes,
1179 slots.cwd
1180 );
1181 } catch (err) {
1182 err.message += `\nOccurred while linting ${options.filename}`;
1183 debug("An error occurred while traversing");
1184 debug("Filename:", options.filename);
1185 if (err.currentNode) {
1186 const { line } = err.currentNode.loc.start;
1187
1188 debug("Line:", line);
1189 err.message += `:${line}`;
1190 }
1191 debug("Parser Options:", parserOptions);
1192 debug("Parser Path:", parserName);
1193 debug("Settings:", settings);
1194 throw err;
1195 }
1196
1197 return applyDisableDirectives({
1198 directives: commentDirectives.disableDirectives,
1199 problems: lintingProblems
1200 .concat(commentDirectives.problems)
1201 .sort((problemA, problemB) => problemA.line - problemB.line || problemA.column - problemB.column),
1202 reportUnusedDisableDirectives: options.reportUnusedDisableDirectives
1203 });
1204 }
1205
1206 /**
1207 * Verifies the text against the rules specified by the second argument.
1208 * @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object.
1209 * @param {ConfigData|ConfigArray} config An ESLintConfig instance to configure everything.
1210 * @param {(string|(VerifyOptions&ProcessorOptions))} [filenameOrOptions] The optional filename of the file being checked.
1211 * If this is not set, the filename will default to '<input>' in the rule context. If
1212 * an object, then it has "filename", "allowInlineConfig", and some properties.
1213 * @returns {LintMessage[]} The results as an array of messages or an empty array if no messages.
1214 */
1215 verify(textOrSourceCode, config, filenameOrOptions) {
1216 debug("Verify");
1217 const options = typeof filenameOrOptions === "string"
1218 ? { filename: filenameOrOptions }
1219 : filenameOrOptions || {};
1220
1221 // CLIEngine passes a `ConfigArray` object.
1222 if (config && typeof config.extractConfig === "function") {
1223 return this._verifyWithConfigArray(textOrSourceCode, config, options);
1224 }
1225
1226 /*
1227 * `Linter` doesn't support `overrides` property in configuration.
1228 * So we cannot apply multiple processors.
1229 */
1230 if (options.preprocess || options.postprocess) {
1231 return this._verifyWithProcessor(textOrSourceCode, config, options);
1232 }
1233 return this._verifyWithoutProcessors(textOrSourceCode, config, options);
1234 }
1235
1236 /**
1237 * Verify a given code with `ConfigArray`.
1238 * @param {string|SourceCode} textOrSourceCode The source code.
1239 * @param {ConfigArray} configArray The config array.
1240 * @param {VerifyOptions&ProcessorOptions} options The options.
1241 * @returns {LintMessage[]} The found problems.
1242 */
1243 _verifyWithConfigArray(textOrSourceCode, configArray, options) {
1244 debug("With ConfigArray: %s", options.filename);
1245
1246 // Store the config array in order to get plugin envs and rules later.
1247 internalSlotsMap.get(this).lastConfigArray = configArray;
1248
1249 // Extract the final config for this file.
1250 const config = configArray.extractConfig(options.filename);
1251 const processor =
1252 config.processor &&
1253 configArray.pluginProcessors.get(config.processor);
1254
1255 // Verify.
1256 if (processor) {
1257 debug("Apply the processor: %o", config.processor);
1258 const { preprocess, postprocess, supportsAutofix } = processor;
1259 const disableFixes = options.disableFixes || !supportsAutofix;
1260
1261 return this._verifyWithProcessor(
1262 textOrSourceCode,
1263 config,
1264 { ...options, disableFixes, postprocess, preprocess },
1265 configArray
1266 );
1267 }
1268 return this._verifyWithoutProcessors(textOrSourceCode, config, options);
1269 }
1270
1271 /**
1272 * Verify with a processor.
1273 * @param {string|SourceCode} textOrSourceCode The source code.
1274 * @param {ConfigData|ExtractedConfig} config The config array.
1275 * @param {VerifyOptions&ProcessorOptions} options The options.
1276 * @param {ConfigArray} [configForRecursive] The `ConfigArray` object to apply multiple processors recursively.
1277 * @returns {LintMessage[]} The found problems.
1278 */
1279 _verifyWithProcessor(textOrSourceCode, config, options, configForRecursive) {
1280 const filename = options.filename || "<input>";
1281 const filenameToExpose = normalizeFilename(filename);
1282 const text = ensureText(textOrSourceCode);
1283 const preprocess = options.preprocess || (rawText => [rawText]);
1284 const postprocess = options.postprocess || lodash.flatten;
1285 const filterCodeBlock =
1286 options.filterCodeBlock ||
1287 (blockFilename => blockFilename.endsWith(".js"));
1288 const originalExtname = path.extname(filename);
1289 const messageLists = preprocess(text, filenameToExpose).map((block, i) => {
1290 debug("A code block was found: %o", block.filename || "(unnamed)");
1291
1292 // Keep the legacy behavior.
1293 if (typeof block === "string") {
1294 return this._verifyWithoutProcessors(block, config, options);
1295 }
1296
1297 const blockText = block.text;
1298 const blockName = path.join(filename, `${i}_${block.filename}`);
1299
1300 // Skip this block if filtered.
1301 if (!filterCodeBlock(blockName, blockText)) {
1302 debug("This code block was skipped.");
1303 return [];
1304 }
1305
1306 // Resolve configuration again if the file extension was changed.
1307 if (configForRecursive && path.extname(blockName) !== originalExtname) {
1308 debug("Resolving configuration again because the file extension was changed.");
1309 return this._verifyWithConfigArray(
1310 blockText,
1311 configForRecursive,
1312 { ...options, filename: blockName }
1313 );
1314 }
1315
1316 // Does lint.
1317 return this._verifyWithoutProcessors(
1318 blockText,
1319 config,
1320 { ...options, filename: blockName }
1321 );
1322 });
1323
1324 return postprocess(messageLists, filenameToExpose);
1325 }
1326
1327 /**
1328 * Gets the SourceCode object representing the parsed source.
1329 * @returns {SourceCode} The SourceCode object.
1330 */
1331 getSourceCode() {
1332 return internalSlotsMap.get(this).lastSourceCode;
1333 }
1334
1335 /**
1336 * Defines a new linting rule.
1337 * @param {string} ruleId A unique rule identifier
1338 * @param {Function | Rule} ruleModule Function from context to object mapping AST node types to event handlers
1339 * @returns {void}
1340 */
1341 defineRule(ruleId, ruleModule) {
1342 internalSlotsMap.get(this).ruleMap.define(ruleId, ruleModule);
1343 }
1344
1345 /**
1346 * Defines many new linting rules.
1347 * @param {Record<string, Function | Rule>} rulesToDefine map from unique rule identifier to rule
1348 * @returns {void}
1349 */
1350 defineRules(rulesToDefine) {
1351 Object.getOwnPropertyNames(rulesToDefine).forEach(ruleId => {
1352 this.defineRule(ruleId, rulesToDefine[ruleId]);
1353 });
1354 }
1355
1356 /**
1357 * Gets an object with all loaded rules.
1358 * @returns {Map<string, Rule>} All loaded rules
1359 */
1360 getRules() {
1361 const { lastConfigArray, ruleMap } = internalSlotsMap.get(this);
1362
1363 return new Map(function *() {
1364 yield* ruleMap;
1365
1366 if (lastConfigArray) {
1367 yield* lastConfigArray.pluginRules;
1368 }
1369 }());
1370 }
1371
1372 /**
1373 * Define a new parser module
1374 * @param {string} parserId Name of the parser
1375 * @param {Parser} parserModule The parser object
1376 * @returns {void}
1377 */
1378 defineParser(parserId, parserModule) {
1379 internalSlotsMap.get(this).parserMap.set(parserId, parserModule);
1380 }
1381
1382 /**
1383 * Performs multiple autofix passes over the text until as many fixes as possible
1384 * have been applied.
1385 * @param {string} text The source text to apply fixes to.
1386 * @param {ConfigData|ConfigArray} config The ESLint config object to use.
1387 * @param {VerifyOptions&ProcessorOptions&FixOptions} options The ESLint options object to use.
1388 * @returns {{fixed:boolean,messages:LintMessage[],output:string}} The result of the fix operation as returned from the
1389 * SourceCodeFixer.
1390 */
1391 verifyAndFix(text, config, options) {
1392 let messages = [],
1393 fixedResult,
1394 fixed = false,
1395 passNumber = 0,
1396 currentText = text;
1397 const debugTextDescription = options && options.filename || `${text.slice(0, 10)}...`;
1398 const shouldFix = options && typeof options.fix !== "undefined" ? options.fix : true;
1399
1400 /**
1401 * This loop continues until one of the following is true:
1402 *
1403 * 1. No more fixes have been applied.
1404 * 2. Ten passes have been made.
1405 *
1406 * That means anytime a fix is successfully applied, there will be another pass.
1407 * Essentially, guaranteeing a minimum of two passes.
1408 */
1409 do {
1410 passNumber++;
1411
1412 debug(`Linting code for ${debugTextDescription} (pass ${passNumber})`);
1413 messages = this.verify(currentText, config, options);
1414
1415 debug(`Generating fixed text for ${debugTextDescription} (pass ${passNumber})`);
1416 fixedResult = SourceCodeFixer.applyFixes(currentText, messages, shouldFix);
1417
1418 /*
1419 * stop if there are any syntax errors.
1420 * 'fixedResult.output' is a empty string.
1421 */
1422 if (messages.length === 1 && messages[0].fatal) {
1423 break;
1424 }
1425
1426 // keep track if any fixes were ever applied - important for return value
1427 fixed = fixed || fixedResult.fixed;
1428
1429 // update to use the fixed output instead of the original text
1430 currentText = fixedResult.output;
1431
1432 } while (
1433 fixedResult.fixed &&
1434 passNumber < MAX_AUTOFIX_PASSES
1435 );
1436
1437 /*
1438 * If the last result had fixes, we need to lint again to be sure we have
1439 * the most up-to-date information.
1440 */
1441 if (fixedResult.fixed) {
1442 fixedResult.messages = this.verify(currentText, config, options);
1443 }
1444
1445 // ensure the last result properly reflects if fixes were done
1446 fixedResult.fixed = fixed;
1447 fixedResult.output = currentText;
1448
1449 return fixedResult;
1450 }
1451}
1452
1453module.exports = {
1454 Linter,
1455
1456 /**
1457 * Get the internal slots of a given Linter instance for tests.
1458 * @param {Linter} instance The Linter instance to get.
1459 * @returns {LinterInternalSlots} The internal slots.
1460 */
1461 getLinterInternalSlots(instance) {
1462 return internalSlotsMap.get(instance);
1463 }
1464};