UNPKG

24.1 kBJavaScriptView Raw
1/**
2 * @fileoverview Common utils for AST.
3 * @author Gyandeep Singh
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const esutils = require("esutils");
13
14//------------------------------------------------------------------------------
15// Helpers
16//------------------------------------------------------------------------------
17
18const anyFunctionPattern = /^(?:Function(?:Declaration|Expression)|ArrowFunctionExpression)$/;
19const anyLoopPattern = /^(?:DoWhile|For|ForIn|ForOf|While)Statement$/;
20const arrayOrTypedArrayPattern = /Array$/;
21const arrayMethodPattern = /^(?:every|filter|find|findIndex|forEach|map|some)$/;
22const bindOrCallOrApplyPattern = /^(?:bind|call|apply)$/;
23const breakableTypePattern = /^(?:(?:Do)?While|For(?:In|Of)?|Switch)Statement$/;
24const thisTagPattern = /^[\s\*]*@this/m;
25
26/**
27 * Checks reference if is non initializer and writable.
28 * @param {Reference} reference - A reference to check.
29 * @param {int} index - The index of the reference in the references.
30 * @param {Reference[]} references - The array that the reference belongs to.
31 * @returns {boolean} Success/Failure
32 * @private
33 */
34function isModifyingReference(reference, index, references) {
35 const identifier = reference.identifier;
36
37 /*
38 * Destructuring assignments can have multiple default value, so
39 * possibly there are multiple writeable references for the same
40 * identifier.
41 */
42 const modifyingDifferentIdentifier = index === 0 ||
43 references[index - 1].identifier !== identifier;
44
45 return (identifier &&
46 reference.init === false &&
47 reference.isWrite() &&
48 modifyingDifferentIdentifier
49 );
50}
51
52/**
53 * Checks whether the given string starts with uppercase or not.
54 *
55 * @param {string} s - The string to check.
56 * @returns {boolean} `true` if the string starts with uppercase.
57 */
58function startsWithUpperCase(s) {
59 return s[0] !== s[0].toLocaleLowerCase();
60}
61
62/**
63 * Checks whether or not a node is a constructor.
64 * @param {ASTNode} node - A function node to check.
65 * @returns {boolean} Wehether or not a node is a constructor.
66 */
67function isES5Constructor(node) {
68 return (node.id && startsWithUpperCase(node.id.name));
69}
70
71/**
72 * Finds a function node from ancestors of a node.
73 * @param {ASTNode} node - A start node to find.
74 * @returns {Node|null} A found function node.
75 */
76function getUpperFunction(node) {
77 while (node) {
78 if (anyFunctionPattern.test(node.type)) {
79 return node;
80 }
81 node = node.parent;
82 }
83 return null;
84}
85
86/**
87 * Checks whether or not a node is `null` or `undefined`.
88 * @param {ASTNode} node - A node to check.
89 * @returns {boolean} Whether or not the node is a `null` or `undefined`.
90 * @public
91 */
92function isNullOrUndefined(node) {
93 return (
94 (node.type === "Literal" && node.value === null) ||
95 (node.type === "Identifier" && node.name === "undefined") ||
96 (node.type === "UnaryExpression" && node.operator === "void")
97 );
98}
99
100/**
101 * Checks whether or not a node is callee.
102 * @param {ASTNode} node - A node to check.
103 * @returns {boolean} Whether or not the node is callee.
104 */
105function isCallee(node) {
106 return node.parent.type === "CallExpression" && node.parent.callee === node;
107}
108
109/**
110 * Checks whether or not a node is `Reclect.apply`.
111 * @param {ASTNode} node - A node to check.
112 * @returns {boolean} Whether or not the node is a `Reclect.apply`.
113 */
114function isReflectApply(node) {
115 return (
116 node.type === "MemberExpression" &&
117 node.object.type === "Identifier" &&
118 node.object.name === "Reflect" &&
119 node.property.type === "Identifier" &&
120 node.property.name === "apply" &&
121 node.computed === false
122 );
123}
124
125/**
126 * Checks whether or not a node is `Array.from`.
127 * @param {ASTNode} node - A node to check.
128 * @returns {boolean} Whether or not the node is a `Array.from`.
129 */
130function isArrayFromMethod(node) {
131 return (
132 node.type === "MemberExpression" &&
133 node.object.type === "Identifier" &&
134 arrayOrTypedArrayPattern.test(node.object.name) &&
135 node.property.type === "Identifier" &&
136 node.property.name === "from" &&
137 node.computed === false
138 );
139}
140
141/**
142 * Checks whether or not a node is a method which has `thisArg`.
143 * @param {ASTNode} node - A node to check.
144 * @returns {boolean} Whether or not the node is a method which has `thisArg`.
145 */
146function isMethodWhichHasThisArg(node) {
147 while (node) {
148 if (node.type === "Identifier") {
149 return arrayMethodPattern.test(node.name);
150 }
151 if (node.type === "MemberExpression" && !node.computed) {
152 node = node.property;
153 continue;
154 }
155
156 break;
157 }
158
159 return false;
160}
161
162/**
163 * Checks whether or not a node has a `@this` tag in its comments.
164 * @param {ASTNode} node - A node to check.
165 * @param {SourceCode} sourceCode - A SourceCode instance to get comments.
166 * @returns {boolean} Whether or not the node has a `@this` tag in its comments.
167 */
168function hasJSDocThisTag(node, sourceCode) {
169 const jsdocComment = sourceCode.getJSDocComment(node);
170
171 if (jsdocComment && thisTagPattern.test(jsdocComment.value)) {
172 return true;
173 }
174
175 // Checks `@this` in its leading comments for callbacks,
176 // because callbacks don't have its JSDoc comment.
177 // e.g.
178 // sinon.test(/* @this sinon.Sandbox */function() { this.spy(); });
179 return sourceCode.getComments(node).leading.some(function(comment) {
180 return thisTagPattern.test(comment.value);
181 });
182}
183
184/**
185 * Determines if a node is surrounded by parentheses.
186 * @param {SourceCode} sourceCode The ESLint source code object
187 * @param {ASTNode} node The node to be checked.
188 * @returns {boolean} True if the node is parenthesised.
189 * @private
190 */
191function isParenthesised(sourceCode, node) {
192 const previousToken = sourceCode.getTokenBefore(node),
193 nextToken = sourceCode.getTokenAfter(node);
194
195 return Boolean(previousToken && nextToken) &&
196 previousToken.value === "(" && previousToken.range[1] <= node.range[0] &&
197 nextToken.value === ")" && nextToken.range[0] >= node.range[1];
198}
199
200//------------------------------------------------------------------------------
201// Public Interface
202//------------------------------------------------------------------------------
203
204module.exports = {
205
206 /**
207 * Determines whether two adjacent tokens are on the same line.
208 * @param {Object} left - The left token object.
209 * @param {Object} right - The right token object.
210 * @returns {boolean} Whether or not the tokens are on the same line.
211 * @public
212 */
213 isTokenOnSameLine(left, right) {
214 return left.loc.end.line === right.loc.start.line;
215 },
216
217 isNullOrUndefined,
218 isCallee,
219 isES5Constructor,
220 getUpperFunction,
221 isArrayFromMethod,
222 isParenthesised,
223
224 /**
225 * Checks whether or not a given node is a string literal.
226 * @param {ASTNode} node - A node to check.
227 * @returns {boolean} `true` if the node is a string literal.
228 */
229 isStringLiteral(node) {
230 return (
231 (node.type === "Literal" && typeof node.value === "string") ||
232 node.type === "TemplateLiteral"
233 );
234 },
235
236 /**
237 * Checks whether a given node is a breakable statement or not.
238 * The node is breakable if the node is one of the following type:
239 *
240 * - DoWhileStatement
241 * - ForInStatement
242 * - ForOfStatement
243 * - ForStatement
244 * - SwitchStatement
245 * - WhileStatement
246 *
247 * @param {ASTNode} node - A node to check.
248 * @returns {boolean} `true` if the node is breakable.
249 */
250 isBreakableStatement(node) {
251 return breakableTypePattern.test(node.type);
252 },
253
254 /**
255 * Gets the label if the parent node of a given node is a LabeledStatement.
256 *
257 * @param {ASTNode} node - A node to get.
258 * @returns {string|null} The label or `null`.
259 */
260 getLabel(node) {
261 if (node.parent.type === "LabeledStatement") {
262 return node.parent.label.name;
263 }
264 return null;
265 },
266
267 /**
268 * Gets references which are non initializer and writable.
269 * @param {Reference[]} references - An array of references.
270 * @returns {Reference[]} An array of only references which are non initializer and writable.
271 * @public
272 */
273 getModifyingReferences(references) {
274 return references.filter(isModifyingReference);
275 },
276
277 /**
278 * Validate that a string passed in is surrounded by the specified character
279 * @param {string} val The text to check.
280 * @param {string} character The character to see if it's surrounded by.
281 * @returns {boolean} True if the text is surrounded by the character, false if not.
282 * @private
283 */
284 isSurroundedBy(val, character) {
285 return val[0] === character && val[val.length - 1] === character;
286 },
287
288 /**
289 * Returns whether the provided node is an ESLint directive comment or not
290 * @param {LineComment|BlockComment} node The node to be checked
291 * @returns {boolean} `true` if the node is an ESLint directive comment
292 */
293 isDirectiveComment(node) {
294 const comment = node.value.trim();
295
296 return (
297 node.type === "Line" && comment.indexOf("eslint-") === 0 ||
298 node.type === "Block" && (
299 comment.indexOf("global ") === 0 ||
300 comment.indexOf("eslint ") === 0 ||
301 comment.indexOf("eslint-") === 0
302 )
303 );
304 },
305
306 /**
307 * Gets the trailing statement of a given node.
308 *
309 * if (code)
310 * consequent;
311 *
312 * When taking this `IfStatement`, returns `consequent;` statement.
313 *
314 * @param {ASTNode} A node to get.
315 * @returns {ASTNode|null} The trailing statement's node.
316 */
317 getTrailingStatement: esutils.ast.trailingStatement,
318
319 /**
320 * Finds the variable by a given name in a given scope and its upper scopes.
321 *
322 * @param {escope.Scope} initScope - A scope to start find.
323 * @param {string} name - A variable name to find.
324 * @returns {escope.Variable|null} A found variable or `null`.
325 */
326 getVariableByName(initScope, name) {
327 let scope = initScope;
328
329 while (scope) {
330 const variable = scope.set.get(name);
331
332 if (variable) {
333 return variable;
334 }
335
336 scope = scope.upper;
337 }
338
339 return null;
340 },
341
342 /**
343 * Checks whether or not a given function node is the default `this` binding.
344 *
345 * First, this checks the node:
346 *
347 * - The function name does not start with uppercase (it's a constructor).
348 * - The function does not have a JSDoc comment that has a @this tag.
349 *
350 * Next, this checks the location of the node.
351 * If the location is below, this judges `this` is valid.
352 *
353 * - The location is not on an object literal.
354 * - The location is not assigned to a variable which starts with an uppercase letter.
355 * - The location is not on an ES2015 class.
356 * - Its `bind`/`call`/`apply` method is not called directly.
357 * - The function is not a callback of array methods (such as `.forEach()`) if `thisArg` is given.
358 *
359 * @param {ASTNode} node - A function node to check.
360 * @param {SourceCode} sourceCode - A SourceCode instance to get comments.
361 * @returns {boolean} The function node is the default `this` binding.
362 */
363 isDefaultThisBinding(node, sourceCode) {
364 if (isES5Constructor(node) || hasJSDocThisTag(node, sourceCode)) {
365 return false;
366 }
367 const isAnonymous = node.id === null;
368
369 while (node) {
370 const parent = node.parent;
371
372 switch (parent.type) {
373
374 /*
375 * Looks up the destination.
376 * e.g., obj.foo = nativeFoo || function foo() { ... };
377 */
378 case "LogicalExpression":
379 case "ConditionalExpression":
380 node = parent;
381 break;
382
383 // If the upper function is IIFE, checks the destination of the return value.
384 // e.g.
385 // obj.foo = (function() {
386 // // setup...
387 // return function foo() { ... };
388 // })();
389 case "ReturnStatement": {
390 const func = getUpperFunction(parent);
391
392 if (func === null || !isCallee(func)) {
393 return true;
394 }
395 node = func.parent;
396 break;
397 }
398
399 // e.g.
400 // var obj = { foo() { ... } };
401 // var obj = { foo: function() { ... } };
402 // class A { constructor() { ... } }
403 // class A { foo() { ... } }
404 // class A { get foo() { ... } }
405 // class A { set foo() { ... } }
406 // class A { static foo() { ... } }
407 case "Property":
408 case "MethodDefinition":
409 return parent.value !== node;
410
411 // e.g.
412 // obj.foo = function foo() { ... };
413 // Foo = function() { ... };
414 // [obj.foo = function foo() { ... }] = a;
415 // [Foo = function() { ... }] = a;
416 case "AssignmentExpression":
417 case "AssignmentPattern":
418 if (parent.right === node) {
419 if (parent.left.type === "MemberExpression") {
420 return false;
421 }
422 if (isAnonymous &&
423 parent.left.type === "Identifier" &&
424 startsWithUpperCase(parent.left.name)
425 ) {
426 return false;
427 }
428 }
429 return true;
430
431 // e.g.
432 // var Foo = function() { ... };
433 case "VariableDeclarator":
434 return !(
435 isAnonymous &&
436 parent.init === node &&
437 parent.id.type === "Identifier" &&
438 startsWithUpperCase(parent.id.name)
439 );
440
441 // e.g.
442 // var foo = function foo() { ... }.bind(obj);
443 // (function foo() { ... }).call(obj);
444 // (function foo() { ... }).apply(obj, []);
445 case "MemberExpression":
446 return (
447 parent.object !== node ||
448 parent.property.type !== "Identifier" ||
449 !bindOrCallOrApplyPattern.test(parent.property.name) ||
450 !isCallee(parent) ||
451 parent.parent.arguments.length === 0 ||
452 isNullOrUndefined(parent.parent.arguments[0])
453 );
454
455 // e.g.
456 // Reflect.apply(function() {}, obj, []);
457 // Array.from([], function() {}, obj);
458 // list.forEach(function() {}, obj);
459 case "CallExpression":
460 if (isReflectApply(parent.callee)) {
461 return (
462 parent.arguments.length !== 3 ||
463 parent.arguments[0] !== node ||
464 isNullOrUndefined(parent.arguments[1])
465 );
466 }
467 if (isArrayFromMethod(parent.callee)) {
468 return (
469 parent.arguments.length !== 3 ||
470 parent.arguments[1] !== node ||
471 isNullOrUndefined(parent.arguments[2])
472 );
473 }
474 if (isMethodWhichHasThisArg(parent.callee)) {
475 return (
476 parent.arguments.length !== 2 ||
477 parent.arguments[0] !== node ||
478 isNullOrUndefined(parent.arguments[1])
479 );
480 }
481 return true;
482
483 // Otherwise `this` is default.
484 default:
485 return true;
486 }
487 }
488
489 /* istanbul ignore next */
490 return true;
491 },
492
493 /**
494 * Get the precedence level based on the node type
495 * @param {ASTNode} node node to evaluate
496 * @returns {int} precedence level
497 * @private
498 */
499 getPrecedence(node) {
500 switch (node.type) {
501 case "SequenceExpression":
502 return 0;
503
504 case "AssignmentExpression":
505 case "ArrowFunctionExpression":
506 case "YieldExpression":
507 return 1;
508
509 case "ConditionalExpression":
510 return 3;
511
512 case "LogicalExpression":
513 switch (node.operator) {
514 case "||":
515 return 4;
516 case "&&":
517 return 5;
518
519 // no default
520 }
521
522 /* falls through */
523
524 case "BinaryExpression":
525
526 switch (node.operator) {
527 case "|":
528 return 6;
529 case "^":
530 return 7;
531 case "&":
532 return 8;
533 case "==":
534 case "!=":
535 case "===":
536 case "!==":
537 return 9;
538 case "<":
539 case "<=":
540 case ">":
541 case ">=":
542 case "in":
543 case "instanceof":
544 return 10;
545 case "<<":
546 case ">>":
547 case ">>>":
548 return 11;
549 case "+":
550 case "-":
551 return 12;
552 case "*":
553 case "/":
554 case "%":
555 return 13;
556
557 // no default
558 }
559
560 /* falls through */
561
562 case "UnaryExpression":
563 case "AwaitExpression":
564 return 14;
565
566 case "UpdateExpression":
567 return 15;
568
569 case "CallExpression":
570
571 // IIFE is allowed to have parens in any position (#655)
572 if (node.callee.type === "FunctionExpression") {
573 return -1;
574 }
575 return 16;
576
577 case "NewExpression":
578 return 17;
579
580 // no default
581 }
582 return 18;
583 },
584
585 /**
586 * Checks whether a given node is a loop node or not.
587 * The following types are loop nodes:
588 *
589 * - DoWhileStatement
590 * - ForInStatement
591 * - ForOfStatement
592 * - ForStatement
593 * - WhileStatement
594 *
595 * @param {ASTNode|null} node - A node to check.
596 * @returns {boolean} `true` if the node is a loop node.
597 */
598 isLoop(node) {
599 return Boolean(node && anyLoopPattern.test(node.type));
600 },
601
602 /**
603 * Checks whether a given node is a function node or not.
604 * The following types are function nodes:
605 *
606 * - ArrowFunctionExpression
607 * - FunctionDeclaration
608 * - FunctionExpression
609 *
610 * @param {ASTNode|null} node - A node to check.
611 * @returns {boolean} `true` if the node is a function node.
612 */
613 isFunction(node) {
614 return Boolean(node && anyFunctionPattern.test(node.type));
615 },
616
617 /**
618 * Gets the property name of a given node.
619 * The node can be a MemberExpression, a Property, or a MethodDefinition.
620 *
621 * If the name is dynamic, this returns `null`.
622 *
623 * For examples:
624 *
625 * a.b // => "b"
626 * a["b"] // => "b"
627 * a['b'] // => "b"
628 * a[`b`] // => "b"
629 * a[100] // => "100"
630 * a[b] // => null
631 * a["a" + "b"] // => null
632 * a[tag`b`] // => null
633 * a[`${b}`] // => null
634 *
635 * let a = {b: 1} // => "b"
636 * let a = {["b"]: 1} // => "b"
637 * let a = {['b']: 1} // => "b"
638 * let a = {[`b`]: 1} // => "b"
639 * let a = {[100]: 1} // => "100"
640 * let a = {[b]: 1} // => null
641 * let a = {["a" + "b"]: 1} // => null
642 * let a = {[tag`b`]: 1} // => null
643 * let a = {[`${b}`]: 1} // => null
644 *
645 * @param {ASTNode} node - The node to get.
646 * @returns {string|null} The property name if static. Otherwise, null.
647 */
648 getStaticPropertyName(node) {
649 let prop;
650
651 switch (node && node.type) {
652 case "Property":
653 case "MethodDefinition":
654 prop = node.key;
655 break;
656
657 case "MemberExpression":
658 prop = node.property;
659 break;
660
661 // no default
662 }
663
664 switch (prop && prop.type) {
665 case "Literal":
666 return String(prop.value);
667
668 case "TemplateLiteral":
669 if (prop.expressions.length === 0 && prop.quasis.length === 1) {
670 return prop.quasis[0].value.cooked;
671 }
672 break;
673
674 case "Identifier":
675 if (!node.computed) {
676 return prop.name;
677 }
678 break;
679
680 // no default
681 }
682
683 return null;
684 },
685
686 /**
687 * Get directives from directive prologue of a Program or Function node.
688 * @param {ASTNode} node - The node to check.
689 * @returns {ASTNode[]} The directives found in the directive prologue.
690 */
691 getDirectivePrologue(node) {
692 const directives = [];
693
694 // Directive prologues only occur at the top of files or functions.
695 if (
696 node.type === "Program" ||
697 node.type === "FunctionDeclaration" ||
698 node.type === "FunctionExpression" ||
699
700 // Do not check arrow functions with implicit return.
701 // `() => "use strict";` returns the string `"use strict"`.
702 (node.type === "ArrowFunctionExpression" && node.body.type === "BlockStatement")
703 ) {
704 const statements = node.type === "Program" ? node.body : node.body.body;
705
706 for (const statement of statements) {
707 if (
708 statement.type === "ExpressionStatement" &&
709 statement.expression.type === "Literal"
710 ) {
711 directives.push(statement);
712 } else {
713 break;
714 }
715 }
716 }
717
718 return directives;
719 },
720
721
722 /**
723 * Determines whether this node is a decimal integer literal. If a node is a decimal integer literal, a dot added
724 after the node will be parsed as a decimal point, rather than a property-access dot.
725 * @param {ASTNode} node - The node to check.
726 * @returns {boolean} `true` if this node is a decimal integer.
727 * @example
728 *
729 * 5 // true
730 * 5. // false
731 * 5.0 // false
732 * 05 // false
733 * 0x5 // false
734 * 0b101 // false
735 * 0o5 // false
736 * 5e0 // false
737 * '5' // false
738 */
739 isDecimalInteger(node) {
740 return node.type === "Literal" && typeof node.value === "number" && /^(0|[1-9]\d*)$/.test(node.raw);
741 }
742};