UNPKG

41.3 kBJavaScriptView Raw
1/**
2 * @fileoverview Main Linter Class
3 * @author Gyandeep Singh
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const assert = require("assert"),
13 EventEmitter = require("events").EventEmitter,
14 eslintScope = require("eslint-scope"),
15 levn = require("levn"),
16 blankScriptAST = require("../conf/blank-script.json"),
17 defaultConfig = require("../conf/default-config-options.js"),
18 replacements = require("../conf/replacements.json"),
19 CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"),
20 ConfigOps = require("./config/config-ops"),
21 validator = require("./config/config-validator"),
22 Environments = require("./config/environments"),
23 NodeEventGenerator = require("./util/node-event-generator"),
24 SourceCode = require("./util/source-code"),
25 Traverser = require("./util/traverser"),
26 RuleContext = require("./rule-context"),
27 Rules = require("./rules"),
28 timing = require("./timing"),
29 astUtils = require("./ast-utils"),
30
31 pkg = require("../package.json");
32
33
34//------------------------------------------------------------------------------
35// Typedefs
36//------------------------------------------------------------------------------
37
38/**
39 * The result of a parsing operation from parseForESLint()
40 * @typedef {Object} CustomParseResult
41 * @property {ASTNode} ast The ESTree AST Program node.
42 * @property {Object} services An object containing additional services related
43 * to the parser.
44 */
45
46//------------------------------------------------------------------------------
47// Helpers
48//------------------------------------------------------------------------------
49
50/**
51 * Parses a list of "name:boolean_value" or/and "name" options divided by comma or
52 * whitespace.
53 * @param {string} string The string to parse.
54 * @param {Comment} comment The comment node which has the string.
55 * @returns {Object} Result map object of names and boolean values
56 */
57function parseBooleanConfig(string, comment) {
58 const items = {};
59
60 // Collapse whitespace around `:` and `,` to make parsing easier
61 string = string.replace(/\s*([:,])\s*/g, "$1");
62
63 string.split(/\s|,+/).forEach(name => {
64 if (!name) {
65 return;
66 }
67 const pos = name.indexOf(":");
68 let value;
69
70 if (pos !== -1) {
71 value = name.substring(pos + 1, name.length);
72 name = name.substring(0, pos);
73 }
74
75 items[name] = {
76 value: (value === "true"),
77 comment
78 };
79
80 });
81 return items;
82}
83
84/**
85 * Parses a JSON-like config.
86 * @param {string} string The string to parse.
87 * @param {Object} location Start line and column of comments for potential error message.
88 * @param {Object[]} messages The messages queue for potential error message.
89 * @returns {Object} Result map object
90 */
91function parseJsonConfig(string, location, messages) {
92 let items = {};
93
94 // Parses a JSON-like comment by the same way as parsing CLI option.
95 try {
96 items = levn.parse("Object", string) || {};
97
98 // Some tests say that it should ignore invalid comments such as `/*eslint no-alert:abc*/`.
99 // Also, commaless notations have invalid severity:
100 // "no-alert: 2 no-console: 2" --> {"no-alert": "2 no-console: 2"}
101 // Should ignore that case as well.
102 if (ConfigOps.isEverySeverityValid(items)) {
103 return items;
104 }
105 } catch (ex) {
106
107 // ignore to parse the string by a fallback.
108 }
109
110 // Optionator cannot parse commaless notations.
111 // But we are supporting that. So this is a fallback for that.
112 items = {};
113 string = string.replace(/([a-zA-Z0-9\-/]+):/g, "\"$1\":").replace(/(]|[0-9])\s+(?=")/, "$1,");
114 try {
115 items = JSON.parse(`{${string}}`);
116 } catch (ex) {
117
118 messages.push({
119 ruleId: null,
120 fatal: true,
121 severity: 2,
122 source: null,
123 message: `Failed to parse JSON from '${string}': ${ex.message}`,
124 line: location.start.line,
125 column: location.start.column + 1
126 });
127
128 }
129
130 return items;
131}
132
133/**
134 * Parses a config of values separated by comma.
135 * @param {string} string The string to parse.
136 * @returns {Object} Result map of values and true values
137 */
138function parseListConfig(string) {
139 const items = {};
140
141 // Collapse whitespace around ,
142 string = string.replace(/\s*,\s*/g, ",");
143
144 string.split(/,+/).forEach(name => {
145 name = name.trim();
146 if (!name) {
147 return;
148 }
149 items[name] = true;
150 });
151 return items;
152}
153
154/**
155 * Ensures that variables representing built-in properties of the Global Object,
156 * and any globals declared by special block comments, are present in the global
157 * scope.
158 * @param {ASTNode} program The top node of the AST.
159 * @param {Scope} globalScope The global scope.
160 * @param {Object} config The existing configuration data.
161 * @param {Environments} envContext Env context
162 * @returns {void}
163 */
164function addDeclaredGlobals(program, globalScope, config, envContext) {
165 const declaredGlobals = {},
166 exportedGlobals = {},
167 explicitGlobals = {},
168 builtin = envContext.get("builtin");
169
170 Object.assign(declaredGlobals, builtin);
171
172 Object.keys(config.env).forEach(name => {
173 if (config.env[name]) {
174 const env = envContext.get(name),
175 environmentGlobals = env && env.globals;
176
177 if (environmentGlobals) {
178 Object.assign(declaredGlobals, environmentGlobals);
179 }
180 }
181 });
182
183 Object.assign(exportedGlobals, config.exported);
184 Object.assign(declaredGlobals, config.globals);
185 Object.assign(explicitGlobals, config.astGlobals);
186
187 Object.keys(declaredGlobals).forEach(name => {
188 let variable = globalScope.set.get(name);
189
190 if (!variable) {
191 variable = new eslintScope.Variable(name, globalScope);
192 variable.eslintExplicitGlobal = false;
193 globalScope.variables.push(variable);
194 globalScope.set.set(name, variable);
195 }
196 variable.writeable = declaredGlobals[name];
197 });
198
199 Object.keys(explicitGlobals).forEach(name => {
200 let variable = globalScope.set.get(name);
201
202 if (!variable) {
203 variable = new eslintScope.Variable(name, globalScope);
204 variable.eslintExplicitGlobal = true;
205 variable.eslintExplicitGlobalComment = explicitGlobals[name].comment;
206 globalScope.variables.push(variable);
207 globalScope.set.set(name, variable);
208 }
209 variable.writeable = explicitGlobals[name].value;
210 });
211
212 // mark all exported variables as such
213 Object.keys(exportedGlobals).forEach(name => {
214 const variable = globalScope.set.get(name);
215
216 if (variable) {
217 variable.eslintUsed = true;
218 }
219 });
220
221 /*
222 * "through" contains all references which definitions cannot be found.
223 * Since we augment the global scope using configuration, we need to update
224 * references and remove the ones that were added by configuration.
225 */
226 globalScope.through = globalScope.through.filter(reference => {
227 const name = reference.identifier.name;
228 const variable = globalScope.set.get(name);
229
230 if (variable) {
231
232 /*
233 * Links the variable and the reference.
234 * And this reference is removed from `Scope#through`.
235 */
236 reference.resolved = variable;
237 variable.references.push(reference);
238
239 return false;
240 }
241
242 return true;
243 });
244}
245
246/**
247 * Add data to reporting configuration to disable reporting for list of rules
248 * starting from start location
249 * @param {Object[]} reportingConfig Current reporting configuration
250 * @param {Object} start Position to start
251 * @param {string[]} rulesToDisable List of rules
252 * @returns {void}
253 */
254function disableReporting(reportingConfig, start, rulesToDisable) {
255
256 if (rulesToDisable.length) {
257 rulesToDisable.forEach(rule => {
258 reportingConfig.push({
259 start,
260 end: null,
261 rule
262 });
263 });
264 } else {
265 reportingConfig.push({
266 start,
267 end: null,
268 rule: null
269 });
270 }
271}
272
273/**
274 * Add data to reporting configuration to enable reporting for list of rules
275 * starting from start location
276 * @param {Object[]} reportingConfig Current reporting configuration
277 * @param {Object} start Position to start
278 * @param {string[]} rulesToEnable List of rules
279 * @returns {void}
280 */
281function enableReporting(reportingConfig, start, rulesToEnable) {
282 let i;
283
284 if (rulesToEnable.length) {
285 rulesToEnable.forEach(rule => {
286 for (i = reportingConfig.length - 1; i >= 0; i--) {
287 if (!reportingConfig[i].end && reportingConfig[i].rule === rule) {
288 reportingConfig[i].end = start;
289 break;
290 }
291 }
292 });
293 } else {
294
295 // find all previous disabled locations if they was started as list of rules
296 let prevStart;
297
298 for (i = reportingConfig.length - 1; i >= 0; i--) {
299 if (prevStart && prevStart !== reportingConfig[i].start) {
300 break;
301 }
302
303 if (!reportingConfig[i].end) {
304 reportingConfig[i].end = start;
305 prevStart = reportingConfig[i].start;
306 }
307 }
308 }
309}
310
311/**
312 * Parses comments in file to extract file-specific config of rules, globals
313 * and environments and merges them with global config; also code blocks
314 * where reporting is disabled or enabled and merges them with reporting config.
315 * @param {string} filename The file being checked.
316 * @param {ASTNode} ast The top node of the AST.
317 * @param {Object} config The existing configuration data.
318 * @param {Linter} linterContext Linter context object
319 * @returns {Object} Modified config object
320 */
321function modifyConfigsFromComments(filename, ast, config, linterContext) {
322
323 let commentConfig = {
324 exported: {},
325 astGlobals: {},
326 rules: {},
327 env: {}
328 };
329 const commentRules = {};
330 const messages = linterContext.messages;
331 const reportingConfig = linterContext.reportingConfig;
332
333 ast.comments.forEach(comment => {
334
335 let value = comment.value.trim();
336 const match = /^(eslint(-\w+){0,3}|exported|globals?)(\s|$)/.exec(value);
337
338 if (match) {
339 value = value.substring(match.index + match[1].length);
340
341 if (comment.type === "Block") {
342 switch (match[1]) {
343 case "exported":
344 Object.assign(commentConfig.exported, parseBooleanConfig(value, comment));
345 break;
346
347 case "globals":
348 case "global":
349 Object.assign(commentConfig.astGlobals, parseBooleanConfig(value, comment));
350 break;
351
352 case "eslint-env":
353 Object.assign(commentConfig.env, parseListConfig(value));
354 break;
355
356 case "eslint-disable":
357 disableReporting(reportingConfig, comment.loc.start, Object.keys(parseListConfig(value)));
358 break;
359
360 case "eslint-enable":
361 enableReporting(reportingConfig, comment.loc.start, Object.keys(parseListConfig(value)));
362 break;
363
364 case "eslint": {
365 const items = parseJsonConfig(value, comment.loc, messages);
366
367 Object.keys(items).forEach(name => {
368 const ruleValue = items[name];
369
370 validator.validateRuleOptions(name, ruleValue, `${filename} line ${comment.loc.start.line}`, linterContext.rules);
371 commentRules[name] = ruleValue;
372 });
373 break;
374 }
375
376 // no default
377 }
378 } else { // comment.type === "Line"
379 if (match[1] === "eslint-disable-line") {
380 disableReporting(reportingConfig, { line: comment.loc.start.line, column: 0 }, Object.keys(parseListConfig(value)));
381 enableReporting(reportingConfig, comment.loc.end, Object.keys(parseListConfig(value)));
382 } else if (match[1] === "eslint-disable-next-line") {
383 disableReporting(reportingConfig, comment.loc.start, Object.keys(parseListConfig(value)));
384 enableReporting(reportingConfig, { line: comment.loc.start.line + 2 }, Object.keys(parseListConfig(value)));
385 }
386 }
387 }
388 });
389
390 // apply environment configs
391 Object.keys(commentConfig.env).forEach(name => {
392 const env = linterContext.environments.get(name);
393
394 if (env) {
395 commentConfig = ConfigOps.merge(commentConfig, env);
396 }
397 });
398 Object.assign(commentConfig.rules, commentRules);
399
400 return ConfigOps.merge(config, commentConfig);
401}
402
403/**
404 * Check if message of rule with ruleId should be ignored in location
405 * @param {Object[]} reportingConfig Collection of ignore records
406 * @param {string} ruleId Id of rule
407 * @param {Object} location Location of message
408 * @returns {boolean} True if message should be ignored, false otherwise
409 */
410function isDisabledByReportingConfig(reportingConfig, ruleId, location) {
411
412 for (let i = 0, c = reportingConfig.length; i < c; i++) {
413
414 const ignore = reportingConfig[i];
415
416 if ((!ignore.rule || ignore.rule === ruleId) &&
417 (location.line > ignore.start.line || (location.line === ignore.start.line && location.column >= ignore.start.column)) &&
418 (!ignore.end || (location.line < ignore.end.line || (location.line === ignore.end.line && location.column <= ignore.end.column)))) {
419 return true;
420 }
421 }
422
423 return false;
424}
425
426/**
427 * Normalize ECMAScript version from the initial config
428 * @param {number} ecmaVersion ECMAScript version from the initial config
429 * @param {boolean} isModule Whether the source type is module or not
430 * @returns {number} normalized ECMAScript version
431 */
432function normalizeEcmaVersion(ecmaVersion, isModule) {
433
434 // Need at least ES6 for modules
435 if (isModule && (!ecmaVersion || ecmaVersion < 6)) {
436 ecmaVersion = 6;
437 }
438
439 // Calculate ECMAScript edition number from official year version starting with
440 // ES2015, which corresponds with ES6 (or a difference of 2009).
441 if (ecmaVersion >= 2015) {
442 ecmaVersion -= 2009;
443 }
444
445 return ecmaVersion;
446}
447
448/**
449 * Process initial config to make it safe to extend by file comment config
450 * @param {Object} config Initial config
451 * @param {Environments} envContext Env context
452 * @returns {Object} Processed config
453 */
454function prepareConfig(config, envContext) {
455 config.globals = config.globals || {};
456 const copiedRules = Object.assign({}, defaultConfig.rules);
457 let parserOptions = Object.assign({}, defaultConfig.parserOptions);
458
459 if (typeof config.rules === "object") {
460 Object.keys(config.rules).forEach(k => {
461 const rule = config.rules[k];
462
463 if (rule === null) {
464 throw new Error(`Invalid config for rule '${k}'.`);
465 }
466 if (Array.isArray(rule)) {
467 copiedRules[k] = rule.slice();
468 } else {
469 copiedRules[k] = rule;
470 }
471 });
472 }
473
474 // merge in environment parserOptions
475 if (typeof config.env === "object") {
476 Object.keys(config.env).forEach(envName => {
477 const env = envContext.get(envName);
478
479 if (config.env[envName] && env && env.parserOptions) {
480 parserOptions = ConfigOps.merge(parserOptions, env.parserOptions);
481 }
482 });
483 }
484
485 const preparedConfig = {
486 rules: copiedRules,
487 parser: config.parser || defaultConfig.parser,
488 globals: ConfigOps.merge(defaultConfig.globals, config.globals),
489 env: ConfigOps.merge(defaultConfig.env, config.env || {}),
490 settings: ConfigOps.merge(defaultConfig.settings, config.settings || {}),
491 parserOptions: ConfigOps.merge(parserOptions, config.parserOptions || {})
492 };
493 const isModule = preparedConfig.parserOptions.sourceType === "module";
494
495 if (isModule) {
496
497 // can't have global return inside of modules
498 preparedConfig.parserOptions.ecmaFeatures = Object.assign({}, preparedConfig.parserOptions.ecmaFeatures, { globalReturn: false });
499 }
500
501 preparedConfig.parserOptions.ecmaVersion = normalizeEcmaVersion(preparedConfig.parserOptions.ecmaVersion, isModule);
502
503 return preparedConfig;
504}
505
506/**
507 * Provide a stub rule with a given message
508 * @param {string} message The message to be displayed for the rule
509 * @returns {Function} Stub rule function
510 */
511function createStubRule(message) {
512
513 /**
514 * Creates a fake rule object
515 * @param {Object} context context object for each rule
516 * @returns {Object} collection of node to listen on
517 */
518 function createRuleModule(context) {
519 return {
520 Program(node) {
521 context.report(node, message);
522 }
523 };
524 }
525
526 if (message) {
527 return createRuleModule;
528 }
529 throw new Error("No message passed to stub rule");
530
531}
532
533/**
534 * Provide a rule replacement message
535 * @param {string} ruleId Name of the rule
536 * @returns {string} Message detailing rule replacement
537 */
538function getRuleReplacementMessage(ruleId) {
539 if (ruleId in replacements.rules) {
540 const newRules = replacements.rules[ruleId];
541
542 return `Rule '${ruleId}' was removed and replaced by: ${newRules.join(", ")}`;
543 }
544
545 return null;
546}
547
548const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)\*\//g;
549
550/**
551 * Checks whether or not there is a comment which has "eslint-env *" in a given text.
552 * @param {string} text - A source code text to check.
553 * @returns {Object|null} A result of parseListConfig() with "eslint-env *" comment.
554 */
555function findEslintEnv(text) {
556 let match, retv;
557
558 eslintEnvPattern.lastIndex = 0;
559
560 while ((match = eslintEnvPattern.exec(text))) {
561 retv = Object.assign(retv || {}, parseListConfig(match[1]));
562 }
563
564 return retv;
565}
566
567/**
568 * Strips Unicode BOM from a given text.
569 *
570 * @param {string} text - A text to strip.
571 * @returns {string} The stripped text.
572 */
573function stripUnicodeBOM(text) {
574
575 /*
576 * Check Unicode BOM.
577 * In JavaScript, string data is stored as UTF-16, so BOM is 0xFEFF.
578 * http://www.ecma-international.org/ecma-262/6.0/#sec-unicode-format-control-characters
579 */
580 if (text.charCodeAt(0) === 0xFEFF) {
581 return text.slice(1);
582 }
583 return text;
584}
585
586/**
587 * Get the severity level of a rule (0 - none, 1 - warning, 2 - error)
588 * Returns 0 if the rule config is not valid (an Array or a number)
589 * @param {Array|number} ruleConfig rule configuration
590 * @returns {number} 0, 1, or 2, indicating rule severity
591 */
592function getRuleSeverity(ruleConfig) {
593 if (typeof ruleConfig === "number") {
594 return ruleConfig;
595 } else if (Array.isArray(ruleConfig)) {
596 return ruleConfig[0];
597 }
598 return 0;
599
600}
601
602/**
603 * Get the options for a rule (not including severity), if any
604 * @param {Array|number} ruleConfig rule configuration
605 * @returns {Array} of rule options, empty Array if none
606 */
607function getRuleOptions(ruleConfig) {
608 if (Array.isArray(ruleConfig)) {
609 return ruleConfig.slice(1);
610 }
611 return [];
612
613}
614
615/**
616 * Parses text into an AST. Moved out here because the try-catch prevents
617 * optimization of functions, so it's best to keep the try-catch as isolated
618 * as possible
619 * @param {string} text The text to parse.
620 * @param {Object} config The ESLint configuration object.
621 * @param {string} filePath The path to the file being parsed.
622 * @returns {ASTNode|CustomParseResult} The AST or parse result if successful,
623 * or null if not.
624 * @param {Array<Object>} messages Messages array for the linter object
625 * @returns {*} parsed text if successful otherwise null
626 * @private
627 */
628function parse(text, config, filePath, messages) {
629
630 let parser,
631 parserOptions = {
632 loc: true,
633 range: true,
634 raw: true,
635 tokens: true,
636 comment: true,
637 filePath
638 };
639
640 try {
641 parser = require(config.parser);
642 } catch (ex) {
643 messages.push({
644 ruleId: null,
645 fatal: true,
646 severity: 2,
647 source: null,
648 message: ex.message,
649 line: 0,
650 column: 0
651 });
652
653 return null;
654 }
655
656 // merge in any additional parser options
657 if (config.parserOptions) {
658 parserOptions = Object.assign({}, config.parserOptions, parserOptions);
659 }
660
661 /*
662 * Check for parsing errors first. If there's a parsing error, nothing
663 * else can happen. However, a parsing error does not throw an error
664 * from this method - it's just considered a fatal error message, a
665 * problem that ESLint identified just like any other.
666 */
667 try {
668 if (typeof parser.parseForESLint === "function") {
669 return parser.parseForESLint(text, parserOptions);
670 }
671 return parser.parse(text, parserOptions);
672
673 } catch (ex) {
674
675 // If the message includes a leading line number, strip it:
676 const message = ex.message.replace(/^line \d+:/i, "").trim();
677 const source = (ex.lineNumber) ? SourceCode.splitLines(text)[ex.lineNumber - 1] : null;
678
679 messages.push({
680 ruleId: null,
681 fatal: true,
682 severity: 2,
683 source,
684 message: `Parsing error: ${message}`,
685
686 line: ex.lineNumber,
687 column: ex.column
688 });
689
690 return null;
691 }
692}
693
694//------------------------------------------------------------------------------
695// Public Interface
696//------------------------------------------------------------------------------
697
698/**
699 * Object that is responsible for verifying JavaScript text
700 * @name eslint
701 */
702class Linter extends EventEmitter {
703
704 constructor() {
705 super();
706 this.messages = [];
707 this.currentConfig = null;
708 this.currentScopes = null;
709 this.scopeManager = null;
710 this.currentFilename = null;
711 this.traverser = null;
712 this.reportingConfig = [];
713 this.sourceCode = null;
714 this.version = pkg.version;
715
716 this.rules = new Rules();
717 this.environments = new Environments();
718
719 // set unlimited listeners (see https://github.com/eslint/eslint/issues/524)
720 this.setMaxListeners(0);
721 }
722
723 /**
724 * Resets the internal state of the object.
725 * @returns {void}
726 */
727 reset() {
728 this.removeAllListeners();
729 this.messages = [];
730 this.currentConfig = null;
731 this.currentScopes = null;
732 this.scopeManager = null;
733 this.traverser = null;
734 this.reportingConfig = [];
735 this.sourceCode = null;
736 }
737
738 /**
739 * Configuration object for the `verify` API. A JS representation of the eslintrc files.
740 * @typedef {Object} ESLintConfig
741 * @property {Object} rules The rule configuration to verify against.
742 * @property {string} [parser] Parser to use when generatig the AST.
743 * @property {Object} [parserOptions] Options for the parsed used.
744 * @property {Object} [settings] Global settings passed to each rule.
745 * @property {Object} [env] The environment to verify in.
746 * @property {Object} [globals] Available globals to the code.
747 */
748
749 /**
750 * Verifies the text against the rules specified by the second argument.
751 * @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object.
752 * @param {ESLintConfig} config An ESLintConfig instance to configure everything.
753 * @param {(string|Object)} [filenameOrOptions] The optional filename of the file being checked.
754 * If this is not set, the filename will default to '<input>' in the rule context. If
755 * an object, then it has "filename", "saveState", and "allowInlineConfig" properties.
756 * @param {boolean} [saveState] Indicates if the state from the last run should be saved.
757 * Mostly useful for testing purposes.
758 * @param {boolean} [filenameOrOptions.allowInlineConfig] Allow/disallow inline comments' ability to change config once it is set. Defaults to true if not supplied.
759 * Useful if you want to validate JS without comments overriding rules.
760 * @returns {Object[]} The results as an array of messages or null if no messages.
761 */
762 verify(textOrSourceCode, config, filenameOrOptions, saveState) {
763 const text = (typeof textOrSourceCode === "string") ? textOrSourceCode : null;
764 let ast,
765 parseResult,
766 allowInlineConfig;
767
768 // evaluate arguments
769 if (typeof filenameOrOptions === "object") {
770 this.currentFilename = filenameOrOptions.filename;
771 allowInlineConfig = filenameOrOptions.allowInlineConfig;
772 saveState = filenameOrOptions.saveState;
773 } else {
774 this.currentFilename = filenameOrOptions;
775 }
776
777 if (!saveState) {
778 this.reset();
779 }
780
781 // search and apply "eslint-env *".
782 const envInFile = findEslintEnv(text || textOrSourceCode.text);
783
784 config = Object.assign({}, config);
785
786 if (envInFile) {
787 if (config.env) {
788 config.env = Object.assign({}, config.env, envInFile);
789 } else {
790 config.env = envInFile;
791 }
792 }
793
794 // process initial config to make it safe to extend
795 config = prepareConfig(config, this.environments);
796
797 // only do this for text
798 if (text !== null) {
799
800 // there's no input, just exit here
801 if (text.trim().length === 0) {
802 this.sourceCode = new SourceCode(text, blankScriptAST);
803 return this.messages;
804 }
805
806 parseResult = parse(
807 stripUnicodeBOM(text).replace(astUtils.SHEBANG_MATCHER, (match, captured) => `//${captured}`),
808 config,
809 this.currentFilename,
810 this.messages
811 );
812
813 // if this result is from a parseForESLint() method, normalize
814 if (parseResult && parseResult.ast) {
815 ast = parseResult.ast;
816 } else {
817 ast = parseResult;
818 parseResult = null;
819 }
820
821 if (ast) {
822 this.sourceCode = new SourceCode(text, ast);
823 }
824
825 } else {
826 this.sourceCode = textOrSourceCode;
827 ast = this.sourceCode.ast;
828 }
829
830 // if espree failed to parse the file, there's no sense in setting up rules
831 if (ast) {
832
833 // parse global comments and modify config
834 if (allowInlineConfig !== false) {
835 config = modifyConfigsFromComments(this.currentFilename, ast, config, this);
836 }
837
838 // ensure that severities are normalized in the config
839 ConfigOps.normalize(config);
840
841 // enable appropriate rules
842 Object.keys(config.rules).filter(key => getRuleSeverity(config.rules[key]) > 0).forEach(key => {
843 let ruleCreator;
844
845 ruleCreator = this.rules.get(key);
846
847 if (!ruleCreator) {
848 const replacementMsg = getRuleReplacementMessage(key);
849
850 if (replacementMsg) {
851 ruleCreator = createStubRule(replacementMsg);
852 } else {
853 ruleCreator = createStubRule(`Definition for rule '${key}' was not found`);
854 }
855 this.rules.define(key, ruleCreator);
856 }
857
858 const severity = getRuleSeverity(config.rules[key]);
859 const options = getRuleOptions(config.rules[key]);
860
861 try {
862 const ruleContext = new RuleContext(
863 key, this, severity, options,
864 config.settings, config.parserOptions, config.parser,
865 ruleCreator.meta,
866 (parseResult && parseResult.services ? parseResult.services : {})
867 );
868
869 const rule = ruleCreator.create ? ruleCreator.create(ruleContext)
870 : ruleCreator(ruleContext);
871
872 // add all the selectors from the rule as listeners
873 Object.keys(rule).forEach(selector => {
874 this.on(selector, timing.enabled
875 ? timing.time(key, rule[selector])
876 : rule[selector]
877 );
878 });
879 } catch (ex) {
880 ex.message = `Error while loading rule '${key}': ${ex.message}`;
881 throw ex;
882 }
883 });
884
885 // save config so rules can access as necessary
886 this.currentConfig = config;
887 this.traverser = new Traverser();
888
889 const ecmaFeatures = this.currentConfig.parserOptions.ecmaFeatures || {};
890 const ecmaVersion = this.currentConfig.parserOptions.ecmaVersion || 5;
891
892 // gather scope data that may be needed by the rules
893 this.scopeManager = eslintScope.analyze(ast, {
894 ignoreEval: true,
895 nodejsScope: ecmaFeatures.globalReturn,
896 impliedStrict: ecmaFeatures.impliedStrict,
897 ecmaVersion,
898 sourceType: this.currentConfig.parserOptions.sourceType || "script",
899 fallback: Traverser.getKeys
900 });
901
902 this.currentScopes = this.scopeManager.scopes;
903
904 // augment global scope with declared global variables
905 addDeclaredGlobals(ast, this.currentScopes[0], this.currentConfig, this.environments);
906
907 let eventGenerator = new NodeEventGenerator(this);
908
909 eventGenerator = new CodePathAnalyzer(eventGenerator);
910
911 /*
912 * Each node has a type property. Whenever a particular type of
913 * node is found, an event is fired. This allows any listeners to
914 * automatically be informed that this type of node has been found
915 * and react accordingly.
916 */
917 this.traverser.traverse(ast, {
918 enter(node, parent) {
919 node.parent = parent;
920 eventGenerator.enterNode(node);
921 },
922 leave(node) {
923 eventGenerator.leaveNode(node);
924 }
925 });
926 }
927
928 // sort by line and column
929 this.messages.sort((a, b) => {
930 const lineDiff = a.line - b.line;
931
932 if (lineDiff === 0) {
933 return a.column - b.column;
934 }
935 return lineDiff;
936
937 });
938
939 return this.messages;
940 }
941
942 /**
943 * Reports a message from one of the rules.
944 * @param {string} ruleId The ID of the rule causing the message.
945 * @param {number} severity The severity level of the rule as configured.
946 * @param {ASTNode} node The AST node that the message relates to.
947 * @param {Object=} location An object containing the error line and column
948 * numbers. If location is not provided the node's start location will
949 * be used.
950 * @param {string} message The actual message.
951 * @param {Object} opts Optional template data which produces a formatted message
952 * with symbols being replaced by this object's values.
953 * @param {Object} fix A fix command description.
954 * @param {Object} meta Metadata of the rule
955 * @returns {void}
956 */
957 report(ruleId, severity, node, location, message, opts, fix, meta) {
958 if (node) {
959 assert.strictEqual(typeof node, "object", "Node must be an object");
960 }
961
962 let endLocation;
963
964 if (typeof location === "string") {
965 assert.ok(node, "Node must be provided when reporting error if location is not provided");
966
967 meta = fix;
968 fix = opts;
969 opts = message;
970 message = location;
971 location = node.loc.start;
972 endLocation = node.loc.end;
973 } else {
974 endLocation = location.end;
975 }
976
977 location = location.start || location;
978
979 if (isDisabledByReportingConfig(this.reportingConfig, ruleId, location)) {
980 return;
981 }
982
983 if (opts) {
984 message = message.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (fullMatch, term) => {
985 if (term in opts) {
986 return opts[term];
987 }
988
989 // Preserve old behavior: If parameter name not provided, don't replace it.
990 return fullMatch;
991 });
992 }
993
994 const problem = {
995 ruleId,
996 severity,
997 message,
998 line: location.line,
999 column: location.column + 1, // switch to 1-base instead of 0-base
1000 nodeType: node && node.type,
1001 source: this.sourceCode.lines[location.line - 1] || ""
1002 };
1003
1004 // Define endLine and endColumn if exists.
1005 if (endLocation) {
1006 problem.endLine = endLocation.line;
1007 problem.endColumn = endLocation.column + 1; // switch to 1-base instead of 0-base
1008 }
1009
1010 // ensure there's range and text properties, otherwise it's not a valid fix
1011 if (fix && Array.isArray(fix.range) && (typeof fix.text === "string")) {
1012
1013 // If rule uses fix, has metadata, but has no metadata.fixable, we should throw
1014 if (meta && !meta.fixable) {
1015 throw new Error("Fixable rules should export a `meta.fixable` property.");
1016 }
1017
1018 problem.fix = fix;
1019 }
1020
1021 this.messages.push(problem);
1022 }
1023
1024 /**
1025 * Gets the SourceCode object representing the parsed source.
1026 * @returns {SourceCode} The SourceCode object.
1027 */
1028 getSourceCode() {
1029 return this.sourceCode;
1030 }
1031
1032 /**
1033 * Gets nodes that are ancestors of current node.
1034 * @returns {ASTNode[]} Array of objects representing ancestors.
1035 */
1036 getAncestors() {
1037 return this.traverser.parents();
1038 }
1039
1040 /**
1041 * Gets the scope for the current node.
1042 * @returns {Object} An object representing the current node's scope.
1043 */
1044 getScope() {
1045 const parents = this.traverser.parents();
1046
1047 // Don't do this for Program nodes - they have no parents
1048 if (parents.length) {
1049
1050 // if current node introduces a scope, add it to the list
1051 const current = this.traverser.current();
1052
1053 if (this.currentConfig.parserOptions.ecmaVersion >= 6) {
1054 if (["BlockStatement", "SwitchStatement", "CatchClause", "FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"].indexOf(current.type) >= 0) {
1055 parents.push(current);
1056 }
1057 } else {
1058 if (["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"].indexOf(current.type) >= 0) {
1059 parents.push(current);
1060 }
1061 }
1062
1063 // Ascend the current node's parents
1064 for (let i = parents.length - 1; i >= 0; --i) {
1065
1066 // Get the innermost scope
1067 const scope = this.scopeManager.acquire(parents[i], true);
1068
1069 if (scope) {
1070 if (scope.type === "function-expression-name") {
1071 return scope.childScopes[0];
1072 }
1073 return scope;
1074
1075 }
1076
1077 }
1078
1079 }
1080
1081 return this.currentScopes[0];
1082 }
1083
1084 /**
1085 * Record that a particular variable has been used in code
1086 * @param {string} name The name of the variable to mark as used
1087 * @returns {boolean} True if the variable was found and marked as used,
1088 * false if not.
1089 */
1090 markVariableAsUsed(name) {
1091 const hasGlobalReturn = this.currentConfig.parserOptions.ecmaFeatures && this.currentConfig.parserOptions.ecmaFeatures.globalReturn,
1092 specialScope = hasGlobalReturn || this.currentConfig.parserOptions.sourceType === "module";
1093 let scope = this.getScope(),
1094 i,
1095 len;
1096
1097 // Special Node.js scope means we need to start one level deeper
1098 if (scope.type === "global" && specialScope) {
1099 scope = scope.childScopes[0];
1100 }
1101
1102 do {
1103 const variables = scope.variables;
1104
1105 for (i = 0, len = variables.length; i < len; i++) {
1106 if (variables[i].name === name) {
1107 variables[i].eslintUsed = true;
1108 return true;
1109 }
1110 }
1111 } while ((scope = scope.upper));
1112
1113 return false;
1114 }
1115
1116 /**
1117 * Gets the filename for the currently parsed source.
1118 * @returns {string} The filename associated with the source being parsed.
1119 * Defaults to "<input>" if no filename info is present.
1120 */
1121 getFilename() {
1122 if (typeof this.currentFilename === "string") {
1123 return this.currentFilename;
1124 }
1125 return "<input>";
1126
1127 }
1128
1129 /**
1130 * Defines a new linting rule.
1131 * @param {string} ruleId A unique rule identifier
1132 * @param {Function} ruleModule Function from context to object mapping AST node types to event handlers
1133 * @returns {void}
1134 */
1135 defineRule(ruleId, ruleModule) {
1136 this.rules.define(ruleId, ruleModule);
1137 }
1138
1139 /**
1140 * Defines many new linting rules.
1141 * @param {Object} rulesToDefine map from unique rule identifier to rule
1142 * @returns {void}
1143 */
1144 defineRules(rulesToDefine) {
1145 Object.getOwnPropertyNames(rulesToDefine).forEach(ruleId => {
1146 this.defineRule(ruleId, rulesToDefine[ruleId]);
1147 });
1148 }
1149
1150 /**
1151 * Gets the default eslint configuration.
1152 * @returns {Object} Object mapping rule IDs to their default configurations
1153 */
1154 defaults() { // eslint-disable-line class-methods-use-this
1155 return defaultConfig;
1156 }
1157
1158 /**
1159 * Gets an object with all loaded rules.
1160 * @returns {Map} All loaded rules
1161 */
1162 getRules() {
1163 return this.rules.getAllLoadedRules();
1164 }
1165
1166 /**
1167 * Gets variables that are declared by a specified node.
1168 *
1169 * The variables are its `defs[].node` or `defs[].parent` is same as the specified node.
1170 * Specifically, below:
1171 *
1172 * - `VariableDeclaration` - variables of its all declarators.
1173 * - `VariableDeclarator` - variables.
1174 * - `FunctionDeclaration`/`FunctionExpression` - its function name and parameters.
1175 * - `ArrowFunctionExpression` - its parameters.
1176 * - `ClassDeclaration`/`ClassExpression` - its class name.
1177 * - `CatchClause` - variables of its exception.
1178 * - `ImportDeclaration` - variables of its all specifiers.
1179 * - `ImportSpecifier`/`ImportDefaultSpecifier`/`ImportNamespaceSpecifier` - a variable.
1180 * - others - always an empty array.
1181 *
1182 * @param {ASTNode} node A node to get.
1183 * @returns {eslint-scope.Variable[]} Variables that are declared by the node.
1184 */
1185 getDeclaredVariables(node) {
1186 return (this.scopeManager && this.scopeManager.getDeclaredVariables(node)) || [];
1187 }
1188}
1189
1190// methods that exist on SourceCode object
1191const externalMethods = {
1192 getSource: "getText",
1193 getSourceLines: "getLines",
1194 getAllComments: "getAllComments",
1195 getNodeByRangeIndex: "getNodeByRangeIndex",
1196 getComments: "getComments",
1197 getCommentsBefore: "getCommentsBefore",
1198 getCommentsAfter: "getCommentsAfter",
1199 getCommentsInside: "getCommentsInside",
1200 getJSDocComment: "getJSDocComment",
1201 getFirstToken: "getFirstToken",
1202 getFirstTokens: "getFirstTokens",
1203 getLastToken: "getLastToken",
1204 getLastTokens: "getLastTokens",
1205 getTokenAfter: "getTokenAfter",
1206 getTokenBefore: "getTokenBefore",
1207 getTokenByRangeStart: "getTokenByRangeStart",
1208 getTokens: "getTokens",
1209 getTokensAfter: "getTokensAfter",
1210 getTokensBefore: "getTokensBefore",
1211 getTokensBetween: "getTokensBetween"
1212};
1213
1214// copy over methods
1215Object.keys(externalMethods).forEach(methodName => {
1216 const exMethodName = externalMethods[methodName];
1217
1218 // All functions expected to have less arguments than 5.
1219 Linter.prototype[methodName] = function(a, b, c, d, e) {
1220 if (this.sourceCode) {
1221 return this.sourceCode[exMethodName](a, b, c, d, e);
1222 }
1223 return null;
1224 };
1225});
1226
1227module.exports = Linter;