1 | /**
|
2 | * @fileoverview Main ESLint object.
|
3 | * @author Nicholas C. Zakas
|
4 | */
|
5 | ;
|
6 |
|
7 | //------------------------------------------------------------------------------
|
8 | // Requirements
|
9 | //------------------------------------------------------------------------------
|
10 |
|
11 | var 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 |
|
25 | function 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 | */
|
35 | function 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 | */
|
63 | function 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 | */
|
78 | function 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 | */
|
97 | function 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 | */
|
120 | function 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 | */
|
168 | function 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 | */
|
195 | function 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 | */
|
231 | function 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 | */
|
302 | function 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 | */
|
322 | function 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 | */
|
354 | module.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 | }());
|