1 /*! @author Toru Nagashima <https://github.com/mysticatea> */
2 import evk from 'eslint-visitor-keys';
5 * Get the innermost scope which contains a given location.
6 * @param {Scope} initialScope The initial scope to search.
7 * @param {Node} node The location to search.
8 * @returns {Scope} The innermost scope.
10 function getInnermostScope(initialScope, node) {
11 const location = node.range[0];
13 let scope = initialScope;
17 for (const childScope of scope.childScopes) {
18 const range = childScope.block.range;
20 if (range[0] <= location && location < range[1]) {
32 * Find the variable of a given name.
33 * @param {Scope} initialScope The scope to start finding.
34 * @param {string|Node} nameOrNode The variable name to find. If this is a Node object then it should be an Identifier node.
35 * @returns {Variable|null} The found variable or null.
37 function findVariable(initialScope, nameOrNode) {
39 let scope = initialScope;
41 if (typeof nameOrNode === "string") {
44 name = nameOrNode.name;
45 scope = getInnermostScope(scope, nameOrNode);
48 while (scope != null) {
49 const variable = scope.set.get(name);
50 if (variable != null) {
60 * Negate the result of `this` calling.
61 * @param {Token} token The token to check.
62 * @returns {boolean} `true` if the result of `this(token)` is `false`.
64 function negate0(token) {
65 return !this(token) //eslint-disable-line no-invalid-this
69 * Creates the negate function of the given function.
70 * @param {function(Token):boolean} f - The function to negate.
71 * @returns {function(Token):boolean} Negated function.
74 return negate0.bind(f)
78 * Checks if the given token is an arrow token or not.
79 * @param {Token} token - The token to check.
80 * @returns {boolean} `true` if the token is an arrow token.
82 function isArrowToken(token) {
83 return token.value === "=>" && token.type === "Punctuator"
87 * Checks if the given token is a comma token or not.
88 * @param {Token} token - The token to check.
89 * @returns {boolean} `true` if the token is a comma token.
91 function isCommaToken(token) {
92 return token.value === "," && token.type === "Punctuator"
96 * Checks if the given token is a semicolon token or not.
97 * @param {Token} token - The token to check.
98 * @returns {boolean} `true` if the token is a semicolon token.
100 function isSemicolonToken(token) {
101 return token.value === ";" && token.type === "Punctuator"
105 * Checks if the given token is a colon token or not.
106 * @param {Token} token - The token to check.
107 * @returns {boolean} `true` if the token is a colon token.
109 function isColonToken(token) {
110 return token.value === ":" && token.type === "Punctuator"
114 * Checks if the given token is an opening parenthesis token or not.
115 * @param {Token} token - The token to check.
116 * @returns {boolean} `true` if the token is an opening parenthesis token.
118 function isOpeningParenToken(token) {
119 return token.value === "(" && token.type === "Punctuator"
123 * Checks if the given token is a closing parenthesis token or not.
124 * @param {Token} token - The token to check.
125 * @returns {boolean} `true` if the token is a closing parenthesis token.
127 function isClosingParenToken(token) {
128 return token.value === ")" && token.type === "Punctuator"
132 * Checks if the given token is an opening square bracket token or not.
133 * @param {Token} token - The token to check.
134 * @returns {boolean} `true` if the token is an opening square bracket token.
136 function isOpeningBracketToken(token) {
137 return token.value === "[" && token.type === "Punctuator"
141 * Checks if the given token is a closing square bracket token or not.
142 * @param {Token} token - The token to check.
143 * @returns {boolean} `true` if the token is a closing square bracket token.
145 function isClosingBracketToken(token) {
146 return token.value === "]" && token.type === "Punctuator"
150 * Checks if the given token is an opening brace token or not.
151 * @param {Token} token - The token to check.
152 * @returns {boolean} `true` if the token is an opening brace token.
154 function isOpeningBraceToken(token) {
155 return token.value === "{" && token.type === "Punctuator"
159 * Checks if the given token is a closing brace token or not.
160 * @param {Token} token - The token to check.
161 * @returns {boolean} `true` if the token is a closing brace token.
163 function isClosingBraceToken(token) {
164 return token.value === "}" && token.type === "Punctuator"
168 * Checks if the given token is a comment token or not.
169 * @param {Token} token - The token to check.
170 * @returns {boolean} `true` if the token is a comment token.
172 function isCommentToken(token) {
174 token.type === "Line" ||
175 token.type === "Block" ||
176 token.type === "Shebang"
180 const isNotArrowToken = negate(isArrowToken);
181 const isNotCommaToken = negate(isCommaToken);
182 const isNotSemicolonToken = negate(isSemicolonToken);
183 const isNotColonToken = negate(isColonToken);
184 const isNotOpeningParenToken = negate(isOpeningParenToken);
185 const isNotClosingParenToken = negate(isClosingParenToken);
186 const isNotOpeningBracketToken = negate(isOpeningBracketToken);
187 const isNotClosingBracketToken = negate(isClosingBracketToken);
188 const isNotOpeningBraceToken = negate(isOpeningBraceToken);
189 const isNotClosingBraceToken = negate(isClosingBraceToken);
190 const isNotCommentToken = negate(isCommentToken);
193 * Get the `(` token of the given function node.
194 * @param {Node} node - The function node to get.
195 * @param {SourceCode} sourceCode - The source code object to get tokens.
196 * @returns {Token} `(` token.
198 function getOpeningParenOfParams(node, sourceCode) {
200 ? sourceCode.getTokenAfter(node.id, isOpeningParenToken)
201 : sourceCode.getFirstToken(node, isOpeningParenToken)
205 * Get the location of the given function node for reporting.
206 * @param {Node} node - The function node to get.
207 * @param {SourceCode} sourceCode - The source code object to get tokens.
208 * @returns {string} The location of the function node for reporting.
210 function getFunctionHeadLocation(node, sourceCode) {
211 const parent = node.parent;
215 if (node.type === "ArrowFunctionExpression") {
216 const arrowToken = sourceCode.getTokenBefore(node.body, isArrowToken);
218 start = arrowToken.loc.start;
219 end = arrowToken.loc.end;
221 parent.type === "Property" ||
222 parent.type === "MethodDefinition"
224 start = parent.loc.start;
225 end = getOpeningParenOfParams(node, sourceCode).loc.start;
227 start = node.loc.start;
228 end = getOpeningParenOfParams(node, sourceCode).loc.start;
232 start: Object.assign({}, start),
233 end: Object.assign({}, end),
237 /* globals BigInt, globalThis, global, self, window */
240 typeof globalThis !== "undefined"
242 : typeof self !== "undefined"
244 : typeof window !== "undefined"
246 : typeof global !== "undefined"
250 const builtinNames = Object.freeze(
261 "decodeURIComponent",
263 "encodeURIComponent",
300 const callAllowed = new Set(
303 typeof BigInt === "function" ? BigInt : undefined,
315 ...Object.getOwnPropertyNames(Math)
317 .filter(f => typeof f === "function"),
336 String.fromCodePoint,
342 ].filter(f => typeof f === "function")
344 const callPassThrough = new Set([
346 Object.preventExtensions,
351 * Get the property descriptor.
352 * @param {object} object The object to get.
353 * @param {string|number|symbol} name The property name to get.
355 function getPropertyDescriptor(object, name) {
357 while ((typeof x === "object" || typeof x === "function") && x !== null) {
358 const d = Object.getOwnPropertyDescriptor(x, name);
362 x = Object.getPrototypeOf(x);
368 * Check if a property is getter or not.
369 * @param {object} object The object to check.
370 * @param {string|number|symbol} name The property name to check.
372 function isGetter(object, name) {
373 const d = getPropertyDescriptor(object, name);
374 return d != null && d.get != null
378 * Get the element values of a given node list.
379 * @param {Node[]} nodeList The node list to get values.
380 * @param {Scope|undefined} initialScope The initial scope to find variables.
381 * @returns {any[]|null} The value list if all nodes are constant. Otherwise, null.
383 function getElementValues(nodeList, initialScope) {
384 const valueList = [];
386 for (let i = 0; i < nodeList.length; ++i) {
387 const elementNode = nodeList[i];
389 if (elementNode == null) {
390 valueList.length = i + 1;
391 } else if (elementNode.type === "SpreadElement") {
392 const argument = getStaticValueR(elementNode.argument, initialScope);
393 if (argument == null) {
396 valueList.push(...argument.value);
398 const element = getStaticValueR(elementNode, initialScope);
399 if (element == null) {
402 valueList.push(element.value);
409 const operations = Object.freeze({
410 ArrayExpression(node, initialScope) {
411 const elements = getElementValues(node.elements, initialScope);
412 return elements != null ? { value: elements } : null
415 AssignmentExpression(node, initialScope) {
416 if (node.operator === "=") {
417 return getStaticValueR(node.right, initialScope)
422 //eslint-disable-next-line complexity
423 BinaryExpression(node, initialScope) {
424 if (node.operator === "in" || node.operator === "instanceof") {
429 const left = getStaticValueR(node.left, initialScope);
430 const right = getStaticValueR(node.right, initialScope);
431 if (left != null && right != null) {
432 switch (node.operator) {
434 return { value: left.value == right.value } //eslint-disable-line eqeqeq
436 return { value: left.value != right.value } //eslint-disable-line eqeqeq
438 return { value: left.value === right.value }
440 return { value: left.value !== right.value }
442 return { value: left.value < right.value }
444 return { value: left.value <= right.value }
446 return { value: left.value > right.value }
448 return { value: left.value >= right.value }
450 return { value: left.value << right.value }
452 return { value: left.value >> right.value }
454 return { value: left.value >>> right.value }
456 return { value: left.value + right.value }
458 return { value: left.value - right.value }
460 return { value: left.value * right.value }
462 return { value: left.value / right.value }
464 return { value: left.value % right.value }
466 return { value: Math.pow(left.value, right.value) }
468 return { value: left.value | right.value }
470 return { value: left.value ^ right.value }
472 return { value: left.value & right.value }
481 CallExpression(node, initialScope) {
482 const calleeNode = node.callee;
483 const args = getElementValues(node.arguments, initialScope);
486 if (calleeNode.type === "MemberExpression") {
487 const object = getStaticValueR(calleeNode.object, initialScope);
488 if (object != null) {
490 object.value == null &&
491 (object.optional || node.optional)
493 return { value: undefined, optional: true }
495 const property = calleeNode.computed
496 ? getStaticValueR(calleeNode.property, initialScope)
497 : { value: calleeNode.property.name };
499 if (property != null) {
500 const receiver = object.value;
501 const methodName = property.value;
502 if (callAllowed.has(receiver[methodName])) {
503 return { value: receiver[methodName](...args) }
505 if (callPassThrough.has(receiver[methodName])) {
506 return { value: args[0] }
511 const callee = getStaticValueR(calleeNode, initialScope);
512 if (callee != null) {
513 if (callee.value == null && node.optional) {
514 return { value: undefined, optional: true }
516 const func = callee.value;
517 if (callAllowed.has(func)) {
518 return { value: func(...args) }
520 if (callPassThrough.has(func)) {
521 return { value: args[0] }
530 ConditionalExpression(node, initialScope) {
531 const test = getStaticValueR(node.test, initialScope);
534 ? getStaticValueR(node.consequent, initialScope)
535 : getStaticValueR(node.alternate, initialScope)
540 ExpressionStatement(node, initialScope) {
541 return getStaticValueR(node.expression, initialScope)
544 Identifier(node, initialScope) {
545 if (initialScope != null) {
546 const variable = findVariable(initialScope, node);
551 variable.defs.length === 0 &&
552 builtinNames.has(variable.name) &&
553 variable.name in globalObject
555 return { value: globalObject[variable.name] }
559 if (variable != null && variable.defs.length === 1) {
560 const def = variable.defs[0];
563 def.parent.kind === "const" &&
564 // TODO(mysticatea): don't support destructuring here.
565 def.node.id.type === "Identifier"
567 return getStaticValueR(def.node.init, initialScope)
575 //istanbul ignore if : this is implementation-specific behavior.
576 if ((node.regex != null || node.bigint != null) && node.value == null) {
577 // It was a RegExp/BigInt literal, but Node.js didn't support it.
580 return { value: node.value }
583 LogicalExpression(node, initialScope) {
584 const left = getStaticValueR(node.left, initialScope);
587 (node.operator === "||" && Boolean(left.value) === true) ||
588 (node.operator === "&&" && Boolean(left.value) === false) ||
589 (node.operator === "??" && left.value != null)
594 const right = getStaticValueR(node.right, initialScope);
603 MemberExpression(node, initialScope) {
604 const object = getStaticValueR(node.object, initialScope);
605 if (object != null) {
606 if (object.value == null && (object.optional || node.optional)) {
607 return { value: undefined, optional: true }
609 const property = node.computed
610 ? getStaticValueR(node.property, initialScope)
611 : { value: node.property.name };
613 if (property != null && !isGetter(object.value, property.value)) {
614 return { value: object.value[property.value] }
620 ChainExpression(node, initialScope) {
621 const expression = getStaticValueR(node.expression, initialScope);
622 if (expression != null) {
623 return { value: expression.value }
628 NewExpression(node, initialScope) {
629 const callee = getStaticValueR(node.callee, initialScope);
630 const args = getElementValues(node.arguments, initialScope);
632 if (callee != null && args != null) {
633 const Func = callee.value;
634 if (callAllowed.has(Func)) {
635 return { value: new Func(...args) }
642 ObjectExpression(node, initialScope) {
645 for (const propertyNode of node.properties) {
646 if (propertyNode.type === "Property") {
647 if (propertyNode.kind !== "init") {
650 const key = propertyNode.computed
651 ? getStaticValueR(propertyNode.key, initialScope)
652 : { value: propertyNode.key.name };
653 const value = getStaticValueR(propertyNode.value, initialScope);
654 if (key == null || value == null) {
657 object[key.value] = value.value;
659 propertyNode.type === "SpreadElement" ||
660 propertyNode.type === "ExperimentalSpreadProperty"
662 const argument = getStaticValueR(
663 propertyNode.argument,
666 if (argument == null) {
669 Object.assign(object, argument.value);
675 return { value: object }
678 SequenceExpression(node, initialScope) {
679 const last = node.expressions[node.expressions.length - 1];
680 return getStaticValueR(last, initialScope)
683 TaggedTemplateExpression(node, initialScope) {
684 const tag = getStaticValueR(node.tag, initialScope);
685 const expressions = getElementValues(
686 node.quasi.expressions,
690 if (tag != null && expressions != null) {
691 const func = tag.value;
692 const strings = node.quasi.quasis.map(q => q.value.cooked);
693 strings.raw = node.quasi.quasis.map(q => q.value.raw);
695 if (func === String.raw) {
696 return { value: func(strings, ...expressions) }
703 TemplateLiteral(node, initialScope) {
704 const expressions = getElementValues(node.expressions, initialScope);
705 if (expressions != null) {
706 let value = node.quasis[0].value.cooked;
707 for (let i = 0; i < expressions.length; ++i) {
708 value += expressions[i];
709 value += node.quasis[i + 1].value.cooked;
716 UnaryExpression(node, initialScope) {
717 if (node.operator === "delete") {
721 if (node.operator === "void") {
722 return { value: undefined }
725 const arg = getStaticValueR(node.argument, initialScope);
727 switch (node.operator) {
729 return { value: -arg.value }
731 return { value: +arg.value } //eslint-disable-line no-implicit-coercion
733 return { value: !arg.value }
735 return { value: ~arg.value }
737 return { value: typeof arg.value }
748 * Get the value of a given node if it's a static value.
749 * @param {Node} node The node to get.
750 * @param {Scope|undefined} initialScope The scope to start finding variable.
751 * @returns {{value:any}|{value:undefined,optional?:true}|null} The static value of the node, or `null`.
753 function getStaticValueR(node, initialScope) {
754 if (node != null && Object.hasOwnProperty.call(operations, node.type)) {
755 return operations[node.type](node, initialScope)
761 * Get the value of a given node if it's a static value.
762 * @param {Node} node The node to get.
763 * @param {Scope} [initialScope] The scope to start finding variable. Optional. If this scope was given, this tries to resolve identifier references which are in the given node as much as possible.
764 * @returns {{value:any}|{value:undefined,optional?:true}|null} The static value of the node, or `null`.
766 function getStaticValue(node, initialScope = null) {
768 return getStaticValueR(node, initialScope)
775 * Get the value of a given node if it's a literal or a template literal.
776 * @param {Node} node The node to get.
777 * @param {Scope} [initialScope] The scope to start finding variable. Optional. If the node is an Identifier node and this scope was given, this checks the variable of the identifier, and returns the value of it if the variable is a constant.
778 * @returns {string|null} The value of the node, or `null`.
780 function getStringIfConstant(node, initialScope = null) {
781 // Handle the literals that the platform doesn't support natively.
782 if (node && node.type === "Literal" && node.value === null) {
784 return `/${node.regex.pattern}/${node.regex.flags}`
791 const evaluated = getStaticValue(node, initialScope);
792 return evaluated && String(evaluated.value)
796 * Get the property name from a MemberExpression node or a Property node.
797 * @param {Node} node The node to get.
798 * @param {Scope} [initialScope] The scope to start finding variable. Optional. If the node is a computed property node and this scope was given, this checks the computed property name by the `getStringIfConstant` function with the scope, and returns the value of it.
799 * @returns {string|null} The property name of the node.
801 function getPropertyName(node, initialScope) {
803 case "MemberExpression":
805 return getStringIfConstant(node.property, initialScope)
807 return node.property.name
810 case "MethodDefinition":
812 return getStringIfConstant(node.key, initialScope)
814 if (node.key.type === "Literal") {
815 return String(node.key.value)
826 * Get the name and kind of the given function node.
827 * @param {ASTNode} node - The function node to get.
828 * @returns {string} The name and kind of the function node.
830 function getFunctionNameWithKind(node) {
831 const parent = node.parent;
834 if (parent.type === "MethodDefinition" && parent.static) {
835 tokens.push("static");
838 tokens.push("async");
840 if (node.generator) {
841 tokens.push("generator");
844 if (node.type === "ArrowFunctionExpression") {
845 tokens.push("arrow", "function");
847 parent.type === "Property" ||
848 parent.type === "MethodDefinition"
850 if (parent.kind === "constructor") {
853 if (parent.kind === "get") {
854 tokens.push("getter");
855 } else if (parent.kind === "set") {
856 tokens.push("setter");
858 tokens.push("method");
861 tokens.push("function");
865 tokens.push(`'${node.id.name}'`);
867 const name = getPropertyName(parent);
870 tokens.push(`'${name}'`);
874 if (node.type === "ArrowFunctionExpression") {
876 parent.type === "VariableDeclarator" &&
878 parent.id.type === "Identifier"
880 tokens.push(`'${parent.id.name}'`);
883 parent.type === "AssignmentExpression" &&
885 parent.left.type === "Identifier"
887 tokens.push(`'${parent.left.name}'`);
891 return tokens.join(" ")
894 const typeConversionBinaryOps = Object.freeze(
916 const typeConversionUnaryOps = Object.freeze(new Set(["-", "+", "!", "~"]));
919 * Check whether the given value is an ASTNode or not.
920 * @param {any} x The value to check.
921 * @returns {boolean} `true` if the value is an ASTNode.
924 return x !== null && typeof x === "object" && typeof x.type === "string"
927 const visitor = Object.freeze(
928 Object.assign(Object.create(null), {
929 $visit(node, options, visitorKeys) {
930 const { type } = node;
932 if (typeof this[type] === "function") {
933 return this[type](node, options, visitorKeys)
936 return this.$visitChildren(node, options, visitorKeys)
939 $visitChildren(node, options, visitorKeys) {
940 const { type } = node;
942 for (const key of visitorKeys[type] || evk.getKeys(node)) {
943 const value = node[key];
945 if (Array.isArray(value)) {
946 for (const element of value) {
949 this.$visit(element, options, visitorKeys)
956 this.$visit(value, options, visitorKeys)
965 ArrowFunctionExpression() {
968 AssignmentExpression() {
974 BinaryExpression(node, options, visitorKeys) {
976 options.considerImplicitTypeConversion &&
977 typeConversionBinaryOps.has(node.operator) &&
978 (node.left.type !== "Literal" || node.right.type !== "Literal")
982 return this.$visitChildren(node, options, visitorKeys)
987 FunctionExpression() {
993 MemberExpression(node, options, visitorKeys) {
994 if (options.considerGetters) {
998 options.considerImplicitTypeConversion &&
1000 node.property.type !== "Literal"
1004 return this.$visitChildren(node, options, visitorKeys)
1006 MethodDefinition(node, options, visitorKeys) {
1008 options.considerImplicitTypeConversion &&
1010 node.key.type !== "Literal"
1014 return this.$visitChildren(node, options, visitorKeys)
1019 Property(node, options, visitorKeys) {
1021 options.considerImplicitTypeConversion &&
1023 node.key.type !== "Literal"
1027 return this.$visitChildren(node, options, visitorKeys)
1029 UnaryExpression(node, options, visitorKeys) {
1030 if (node.operator === "delete") {
1034 options.considerImplicitTypeConversion &&
1035 typeConversionUnaryOps.has(node.operator) &&
1036 node.argument.type !== "Literal"
1040 return this.$visitChildren(node, options, visitorKeys)
1042 UpdateExpression() {
1052 * Check whether a given node has any side effect or not.
1053 * @param {Node} node The node to get.
1054 * @param {SourceCode} sourceCode The source code object.
1055 * @param {object} [options] The option object.
1056 * @param {boolean} [options.considerGetters=false] If `true` then it considers member accesses as the node which has side effects.
1057 * @param {boolean} [options.considerImplicitTypeConversion=false] If `true` then it considers implicit type conversion as the node which has side effects.
1058 * @param {object} [options.visitorKeys=evk.KEYS] The keys to traverse nodes. Use `context.getSourceCode().visitorKeys`.
1059 * @returns {boolean} `true` if the node has a certain side effect.
1061 function hasSideEffect(
1064 { considerGetters = false, considerImplicitTypeConversion = false } = {}
1066 return visitor.$visit(
1068 { considerGetters, considerImplicitTypeConversion },
1069 sourceCode.visitorKeys || evk.KEYS
1074 * Get the left parenthesis of the parent node syntax if it exists.
1075 * E.g., `if (a) {}` then the `(`.
1076 * @param {Node} node The AST node to check.
1077 * @param {SourceCode} sourceCode The source code object to get tokens.
1078 * @returns {Token|null} The left parenthesis of the parent node syntax
1080 function getParentSyntaxParen(node, sourceCode) {
1081 const parent = node.parent;
1083 switch (parent.type) {
1084 case "CallExpression":
1085 case "NewExpression":
1086 if (parent.arguments.length === 1 && parent.arguments[0] === node) {
1087 return sourceCode.getTokenAfter(
1094 case "DoWhileStatement":
1095 if (parent.test === node) {
1096 return sourceCode.getTokenAfter(
1104 case "WhileStatement":
1105 if (parent.test === node) {
1106 return sourceCode.getFirstToken(parent, 1)
1110 case "ImportExpression":
1111 if (parent.source === node) {
1112 return sourceCode.getFirstToken(parent, 1)
1116 case "SwitchStatement":
1117 if (parent.discriminant === node) {
1118 return sourceCode.getFirstToken(parent, 1)
1122 case "WithStatement":
1123 if (parent.object === node) {
1124 return sourceCode.getFirstToken(parent, 1)
1134 * Check whether a given node is parenthesized or not.
1135 * @param {number} times The number of parantheses.
1136 * @param {Node} node The AST node to check.
1137 * @param {SourceCode} sourceCode The source code object to get tokens.
1138 * @returns {boolean} `true` if the node is parenthesized the given times.
1141 * Check whether a given node is parenthesized or not.
1142 * @param {Node} node The AST node to check.
1143 * @param {SourceCode} sourceCode The source code object to get tokens.
1144 * @returns {boolean} `true` if the node is parenthesized.
1146 function isParenthesized(
1151 let times, node, sourceCode, maybeLeftParen, maybeRightParen;
1152 if (typeof timesOrNode === "number") {
1153 times = timesOrNode | 0;
1154 node = nodeOrSourceCode;
1155 sourceCode = optionalSourceCode;
1156 if (!(times >= 1)) {
1157 throw new TypeError("'times' should be a positive integer.")
1162 sourceCode = nodeOrSourceCode;
1169 maybeLeftParen = maybeRightParen = node;
1171 maybeLeftParen = sourceCode.getTokenBefore(maybeLeftParen);
1172 maybeRightParen = sourceCode.getTokenAfter(maybeRightParen);
1174 maybeLeftParen != null &&
1175 maybeRightParen != null &&
1176 isOpeningParenToken(maybeLeftParen) &&
1177 isClosingParenToken(maybeRightParen) &&
1178 // Avoid false positive such as `if (a) {}`
1179 maybeLeftParen !== getParentSyntaxParen(node, sourceCode) &&
1187 * @author Toru Nagashima <https://github.com/mysticatea>
1188 * See LICENSE file in root directory for full license.
1191 const placeholder = /\$(?:[$&`']|[1-9][0-9]?)/gu;
1193 /** @type {WeakMap<PatternMatcher, {pattern:RegExp,escaped:boolean}>} */
1194 const internal = new WeakMap();
1197 * Check whether a given character is escaped or not.
1198 * @param {string} str The string to check.
1199 * @param {number} index The location of the character to check.
1200 * @returns {boolean} `true` if the character is escaped.
1202 function isEscaped(str, index) {
1203 let escaped = false;
1204 for (let i = index - 1; i >= 0 && str.charCodeAt(i) === 0x5c; --i) {
1211 * Replace a given string by a given matcher.
1212 * @param {PatternMatcher} matcher The pattern matcher.
1213 * @param {string} str The string to be replaced.
1214 * @param {string} replacement The new substring to replace each matched part.
1215 * @returns {string} The replaced string.
1217 function replaceS(matcher, str, replacement) {
1221 /** @type {RegExpExecArray} */
1225 * @param {string} key The placeholder.
1226 * @returns {string} The replaced string.
1228 function replacer(key) {
1235 return str.slice(0, match.index)
1237 return str.slice(match.index + match[0].length)
1239 const i = key.slice(1);
1248 for (match of matcher.execAll(str)) {
1249 chunks.push(str.slice(index, match.index));
1250 chunks.push(replacement.replace(placeholder, replacer));
1251 index = match.index + match[0].length;
1253 chunks.push(str.slice(index));
1255 return chunks.join("")
1259 * Replace a given string by a given matcher.
1260 * @param {PatternMatcher} matcher The pattern matcher.
1261 * @param {string} str The string to be replaced.
1262 * @param {(...strs[])=>string} replace The function to replace each matched part.
1263 * @returns {string} The replaced string.
1265 function replaceF(matcher, str, replace) {
1269 for (const match of matcher.execAll(str)) {
1270 chunks.push(str.slice(index, match.index));
1271 chunks.push(String(replace(...match, match.index, match.input)));
1272 index = match.index + match[0].length;
1274 chunks.push(str.slice(index));
1276 return chunks.join("")
1280 * The class to find patterns as considering escape sequences.
1282 class PatternMatcher {
1284 * Initialize this matcher.
1285 * @param {RegExp} pattern The pattern to match.
1286 * @param {{escaped:boolean}} options The options.
1288 constructor(pattern, { escaped = false } = {}) {
1289 if (!(pattern instanceof RegExp)) {
1290 throw new TypeError("'pattern' should be a RegExp instance.")
1292 if (!pattern.flags.includes("g")) {
1293 throw new Error("'pattern' should contains 'g' flag.")
1296 internal.set(this, {
1297 pattern: new RegExp(pattern.source, pattern.flags),
1298 escaped: Boolean(escaped),
1303 * Find the pattern in a given string.
1304 * @param {string} str The string to find.
1305 * @returns {IterableIterator<RegExpExecArray>} The iterator which iterate the matched information.
1308 const { pattern, escaped } = internal.get(this);
1312 pattern.lastIndex = 0;
1313 while ((match = pattern.exec(str)) != null) {
1314 if (escaped || !isEscaped(str, match.index)) {
1315 lastIndex = pattern.lastIndex;
1317 pattern.lastIndex = lastIndex;
1323 * Check whether the pattern is found in a given string.
1324 * @param {string} str The string to check.
1325 * @returns {boolean} `true` if the pattern was found in the string.
1328 const it = this.execAll(str);
1329 const ret = it.next();
1334 * Replace a given string.
1335 * @param {string} str The string to be replaced.
1336 * @param {(string|((...strs:string[])=>string))} replacer The string or function to replace. This is the same as the 2nd argument of `String.prototype.replace`.
1337 * @returns {string} The replaced string.
1339 [Symbol.replace](str, replacer) {
1340 return typeof replacer === "function"
1341 ? replaceF(this, String(str), replacer)
1342 : replaceS(this, String(str), String(replacer))
1346 const IMPORT_TYPE = /^(?:Import|Export(?:All|Default|Named))Declaration$/u;
1347 const has = Function.call.bind(Object.hasOwnProperty);
1349 const READ = Symbol("read");
1350 const CALL = Symbol("call");
1351 const CONSTRUCT = Symbol("construct");
1352 const ESM = Symbol("esm");
1354 const requireCall = { require: { [CALL]: true } };
1357 * Check whether a given variable is modified or not.
1358 * @param {Variable} variable The variable to check.
1359 * @returns {boolean} `true` if the variable is modified.
1361 function isModifiedGlobal(variable) {
1364 variable.defs.length !== 0 ||
1365 variable.references.some(r => r.isWrite())
1370 * Check if the value of a given node is passed through to the parent syntax as-is.
1371 * For example, `a` and `b` in (`a || b` and `c ? a : b`) are passed through.
1372 * @param {Node} node A node to check.
1373 * @returns {boolean} `true` if the node is passed through.
1375 function isPassThrough(node) {
1376 const parent = node.parent;
1378 switch (parent && parent.type) {
1379 case "ConditionalExpression":
1380 return parent.consequent === node || parent.alternate === node
1381 case "LogicalExpression":
1383 case "SequenceExpression":
1384 return parent.expressions[parent.expressions.length - 1] === node
1385 case "ChainExpression":
1394 * The reference tracker.
1396 class ReferenceTracker {
1398 * Initialize this tracker.
1399 * @param {Scope} globalScope The global scope.
1400 * @param {object} [options] The options.
1401 * @param {"legacy"|"strict"} [options.mode="strict"] The mode to determine the ImportDeclaration's behavior for CJS modules.
1402 * @param {string[]} [options.globalObjectNames=["global","globalThis","self","window"]] The variable names for Global Object.
1408 globalObjectNames = ["global", "globalThis", "self", "window"],
1411 this.variableStack = [];
1412 this.globalScope = globalScope;
1414 this.globalObjectNames = globalObjectNames.slice(0);
1418 * Iterate the references of global variables.
1419 * @param {object} traceMap The trace map.
1420 * @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references.
1422 *iterateGlobalReferences(traceMap) {
1423 for (const key of Object.keys(traceMap)) {
1424 const nextTraceMap = traceMap[key];
1426 const variable = this.globalScope.set.get(key);
1428 if (isModifiedGlobal(variable)) {
1432 yield* this._iterateVariableReferences(
1440 for (const key of this.globalObjectNames) {
1442 const variable = this.globalScope.set.get(key);
1444 if (isModifiedGlobal(variable)) {
1448 yield* this._iterateVariableReferences(
1458 * Iterate the references of CommonJS modules.
1459 * @param {object} traceMap The trace map.
1460 * @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references.
1462 *iterateCjsReferences(traceMap) {
1463 for (const { node } of this.iterateGlobalReferences(requireCall)) {
1464 const key = getStringIfConstant(node.arguments[0]);
1465 if (key == null || !has(traceMap, key)) {
1469 const nextTraceMap = traceMap[key];
1472 if (nextTraceMap[READ]) {
1477 info: nextTraceMap[READ],
1480 yield* this._iteratePropertyReferences(node, path, nextTraceMap);
1485 * Iterate the references of ES modules.
1486 * @param {object} traceMap The trace map.
1487 * @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references.
1489 *iterateEsmReferences(traceMap) {
1490 const programNode = this.globalScope.block;
1492 for (const node of programNode.body) {
1493 if (!IMPORT_TYPE.test(node.type) || node.source == null) {
1496 const moduleId = node.source.value;
1498 if (!has(traceMap, moduleId)) {
1501 const nextTraceMap = traceMap[moduleId];
1502 const path = [moduleId];
1504 if (nextTraceMap[READ]) {
1505 yield { node, path, type: READ, info: nextTraceMap[READ] };
1508 if (node.type === "ExportAllDeclaration") {
1509 for (const key of Object.keys(nextTraceMap)) {
1510 const exportTraceMap = nextTraceMap[key];
1511 if (exportTraceMap[READ]) {
1514 path: path.concat(key),
1516 info: exportTraceMap[READ],
1521 for (const specifier of node.specifiers) {
1522 const esm = has(nextTraceMap, ESM);
1523 const it = this._iterateImportReferences(
1528 : this.mode === "legacy"
1530 { default: nextTraceMap },
1533 : { default: nextTraceMap }
1539 for (const report of it) {
1540 report.path = report.path.filter(exceptDefault);
1542 report.path.length >= 2 ||
1543 report.type !== READ
1555 * Iterate the references for a given variable.
1556 * @param {Variable} variable The variable to iterate that references.
1557 * @param {string[]} path The current path.
1558 * @param {object} traceMap The trace map.
1559 * @param {boolean} shouldReport = The flag to report those references.
1560 * @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references.
1562 *_iterateVariableReferences(variable, path, traceMap, shouldReport) {
1563 if (this.variableStack.includes(variable)) {
1566 this.variableStack.push(variable);
1568 for (const reference of variable.references) {
1569 if (!reference.isRead()) {
1572 const node = reference.identifier;
1574 if (shouldReport && traceMap[READ]) {
1575 yield { node, path, type: READ, info: traceMap[READ] };
1577 yield* this._iteratePropertyReferences(node, path, traceMap);
1580 this.variableStack.pop();
1585 * Iterate the references for a given AST node.
1586 * @param rootNode The AST node to iterate references.
1587 * @param {string[]} path The current path.
1588 * @param {object} traceMap The trace map.
1589 * @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references.
1591 //eslint-disable-next-line complexity
1592 *_iteratePropertyReferences(rootNode, path, traceMap) {
1593 let node = rootNode;
1594 while (isPassThrough(node)) {
1598 const parent = node.parent;
1599 if (parent.type === "MemberExpression") {
1600 if (parent.object === node) {
1601 const key = getPropertyName(parent);
1602 if (key == null || !has(traceMap, key)) {
1606 path = path.concat(key); //eslint-disable-line no-param-reassign
1607 const nextTraceMap = traceMap[key];
1608 if (nextTraceMap[READ]) {
1613 info: nextTraceMap[READ],
1616 yield* this._iteratePropertyReferences(
1624 if (parent.type === "CallExpression") {
1625 if (parent.callee === node && traceMap[CALL]) {
1626 yield { node: parent, path, type: CALL, info: traceMap[CALL] };
1630 if (parent.type === "NewExpression") {
1631 if (parent.callee === node && traceMap[CONSTRUCT]) {
1636 info: traceMap[CONSTRUCT],
1641 if (parent.type === "AssignmentExpression") {
1642 if (parent.right === node) {
1643 yield* this._iterateLhsReferences(parent.left, path, traceMap);
1644 yield* this._iteratePropertyReferences(parent, path, traceMap);
1648 if (parent.type === "AssignmentPattern") {
1649 if (parent.right === node) {
1650 yield* this._iterateLhsReferences(parent.left, path, traceMap);
1654 if (parent.type === "VariableDeclarator") {
1655 if (parent.init === node) {
1656 yield* this._iterateLhsReferences(parent.id, path, traceMap);
1662 * Iterate the references for a given Pattern node.
1663 * @param {Node} patternNode The Pattern node to iterate references.
1664 * @param {string[]} path The current path.
1665 * @param {object} traceMap The trace map.
1666 * @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references.
1668 *_iterateLhsReferences(patternNode, path, traceMap) {
1669 if (patternNode.type === "Identifier") {
1670 const variable = findVariable(this.globalScope, patternNode);
1671 if (variable != null) {
1672 yield* this._iterateVariableReferences(
1681 if (patternNode.type === "ObjectPattern") {
1682 for (const property of patternNode.properties) {
1683 const key = getPropertyName(property);
1685 if (key == null || !has(traceMap, key)) {
1689 const nextPath = path.concat(key);
1690 const nextTraceMap = traceMap[key];
1691 if (nextTraceMap[READ]) {
1696 info: nextTraceMap[READ],
1699 yield* this._iterateLhsReferences(
1707 if (patternNode.type === "AssignmentPattern") {
1708 yield* this._iterateLhsReferences(patternNode.left, path, traceMap);
1713 * Iterate the references for a given ModuleSpecifier node.
1714 * @param {Node} specifierNode The ModuleSpecifier node to iterate references.
1715 * @param {string[]} path The current path.
1716 * @param {object} traceMap The trace map.
1717 * @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references.
1719 *_iterateImportReferences(specifierNode, path, traceMap) {
1720 const type = specifierNode.type;
1722 if (type === "ImportSpecifier" || type === "ImportDefaultSpecifier") {
1724 type === "ImportDefaultSpecifier"
1726 : specifierNode.imported.name;
1727 if (!has(traceMap, key)) {
1731 path = path.concat(key); //eslint-disable-line no-param-reassign
1732 const nextTraceMap = traceMap[key];
1733 if (nextTraceMap[READ]) {
1735 node: specifierNode,
1738 info: nextTraceMap[READ],
1741 yield* this._iterateVariableReferences(
1742 findVariable(this.globalScope, specifierNode.local),
1751 if (type === "ImportNamespaceSpecifier") {
1752 yield* this._iterateVariableReferences(
1753 findVariable(this.globalScope, specifierNode.local),
1761 if (type === "ExportSpecifier") {
1762 const key = specifierNode.local.name;
1763 if (!has(traceMap, key)) {
1767 path = path.concat(key); //eslint-disable-line no-param-reassign
1768 const nextTraceMap = traceMap[key];
1769 if (nextTraceMap[READ]) {
1771 node: specifierNode,
1774 info: nextTraceMap[READ],
1781 ReferenceTracker.READ = READ;
1782 ReferenceTracker.CALL = CALL;
1783 ReferenceTracker.CONSTRUCT = CONSTRUCT;
1784 ReferenceTracker.ESM = ESM;
1787 * This is a predicate function for Array#filter.
1788 * @param {string} name A name part.
1789 * @param {number} index The index of the name.
1790 * @returns {boolean} `false` if it's default.
1792 function exceptDefault(name, index) {
1793 return !(index === 1 && name === "default")
1801 getFunctionHeadLocation,
1802 getFunctionNameWithKind,
1806 getStringIfConstant,
1809 isClosingBraceToken,
1810 isClosingBracketToken,
1811 isClosingParenToken,
1816 isNotClosingBraceToken,
1817 isNotClosingBracketToken,
1818 isNotClosingParenToken,
1822 isNotOpeningBraceToken,
1823 isNotOpeningBracketToken,
1824 isNotOpeningParenToken,
1825 isNotSemicolonToken,
1826 isOpeningBraceToken,
1827 isOpeningBracketToken,
1828 isOpeningParenToken,
1836 export default index;
1837 export { CALL, CONSTRUCT, ESM, PatternMatcher, READ, ReferenceTracker, findVariable, getFunctionHeadLocation, getFunctionNameWithKind, getInnermostScope, getPropertyName, getStaticValue, getStringIfConstant, hasSideEffect, isArrowToken, isClosingBraceToken, isClosingBracketToken, isClosingParenToken, isColonToken, isCommaToken, isCommentToken, isNotArrowToken, isNotClosingBraceToken, isNotClosingBracketToken, isNotClosingParenToken, isNotColonToken, isNotCommaToken, isNotCommentToken, isNotOpeningBraceToken, isNotOpeningBracketToken, isNotOpeningParenToken, isNotSemicolonToken, isOpeningBraceToken, isOpeningBracketToken, isOpeningParenToken, isParenthesized, isSemicolonToken };
1838 //# sourceMappingURL=index.mjs.map