UNPKG

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