UNPKG

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