UNPKG

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