UNPKG

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