318 lines
15 KiB
Plaintext
318 lines
15 KiB
Plaintext
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.compareNodes = void 0;
|
|
const utils_1 = require("@typescript-eslint/utils");
|
|
const visitor_keys_1 = require("@typescript-eslint/visitor-keys");
|
|
function compareArrays(arrayA, arrayB) {
|
|
if (arrayA.length !== arrayB.length) {
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
const result = arrayA.every((elA, idx) => {
|
|
const elB = arrayB[idx];
|
|
if (elA == null || elB == null) {
|
|
return elA === elB;
|
|
}
|
|
return compareUnknownValues(elA, elB) === "Equal" /* NodeComparisonResult.Equal */;
|
|
});
|
|
if (result) {
|
|
return "Equal" /* NodeComparisonResult.Equal */;
|
|
}
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
function isValidNode(x) {
|
|
return (typeof x === 'object' &&
|
|
x != null &&
|
|
'type' in x &&
|
|
typeof x.type === 'string');
|
|
}
|
|
function isValidChainExpressionToLookThrough(node) {
|
|
return (!(node.parent?.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
node.parent.object === node) &&
|
|
!(node.parent?.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
node.parent.callee === node) &&
|
|
node.type === utils_1.AST_NODE_TYPES.ChainExpression);
|
|
}
|
|
function compareUnknownValues(valueA, valueB) {
|
|
/* istanbul ignore if -- not possible for us to test this - it's just a sanity safeguard */
|
|
if (valueA == null || valueB == null) {
|
|
if (valueA !== valueB) {
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
return "Equal" /* NodeComparisonResult.Equal */;
|
|
}
|
|
/* istanbul ignore if -- not possible for us to test this - it's just a sanity safeguard */
|
|
if (!isValidNode(valueA) || !isValidNode(valueB)) {
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
return compareNodes(valueA, valueB);
|
|
}
|
|
function compareByVisiting(nodeA, nodeB) {
|
|
const currentVisitorKeys = visitor_keys_1.visitorKeys[nodeA.type];
|
|
/* istanbul ignore if -- not possible for us to test this - it's just a sanity safeguard */
|
|
if (currentVisitorKeys == null) {
|
|
// we don't know how to visit this node, so assume it's invalid to avoid false-positives / broken fixers
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
if (currentVisitorKeys.length === 0) {
|
|
// assume nodes with no keys are constant things like keywords
|
|
return "Equal" /* NodeComparisonResult.Equal */;
|
|
}
|
|
for (const key of currentVisitorKeys) {
|
|
// @ts-expect-error - dynamic access but it's safe
|
|
const nodeAChildOrChildren = nodeA[key];
|
|
// @ts-expect-error - dynamic access but it's safe
|
|
const nodeBChildOrChildren = nodeB[key];
|
|
if (Array.isArray(nodeAChildOrChildren)) {
|
|
const arrayA = nodeAChildOrChildren;
|
|
const arrayB = nodeBChildOrChildren;
|
|
const result = compareArrays(arrayA, arrayB);
|
|
if (result !== "Equal" /* NodeComparisonResult.Equal */) {
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
// fallthrough to the next key as the key was "equal"
|
|
}
|
|
else {
|
|
const result = compareUnknownValues(nodeAChildOrChildren, nodeBChildOrChildren);
|
|
if (result !== "Equal" /* NodeComparisonResult.Equal */) {
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
// fallthrough to the next key as the key was "equal"
|
|
}
|
|
}
|
|
return "Equal" /* NodeComparisonResult.Equal */;
|
|
}
|
|
function compareNodesUncached(nodeA, nodeB) {
|
|
if (nodeA.type !== nodeB.type) {
|
|
// special cases where nodes are allowed to be non-equal
|
|
// look through a chain expression node at the top-level because it only
|
|
// exists to delimit the end of an optional chain
|
|
//
|
|
// a?.b && a.b.c
|
|
// ^^^^ ChainExpression, MemberExpression
|
|
// ^^^^^ MemberExpression
|
|
//
|
|
// except for in this class of cases
|
|
// (a?.b).c && a.b.c
|
|
// because the parentheses have runtime meaning (sad face)
|
|
if (isValidChainExpressionToLookThrough(nodeA)) {
|
|
return compareNodes(nodeA.expression, nodeB);
|
|
}
|
|
if (isValidChainExpressionToLookThrough(nodeB)) {
|
|
return compareNodes(nodeA, nodeB.expression);
|
|
}
|
|
// look through the type-only non-null assertion because its existence could
|
|
// possibly be replaced by an optional chain instead
|
|
//
|
|
// a.b! && a.b.c
|
|
// ^^^^ TSNonNullExpression
|
|
if (nodeA.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
|
|
return compareNodes(nodeA.expression, nodeB);
|
|
}
|
|
if (nodeB.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
|
|
return compareNodes(nodeA, nodeB.expression);
|
|
}
|
|
// special case for subset optional chains where the node types don't match,
|
|
// but we want to try comparing by discarding the "extra" code
|
|
//
|
|
// a && a.b
|
|
// ^ compare this
|
|
// a && a()
|
|
// ^ compare this
|
|
// a.b && a.b()
|
|
// ^^^ compare this
|
|
// a() && a().b
|
|
// ^^^ compare this
|
|
// import.meta && import.meta.b
|
|
// ^^^^^^^^^^^ compare this
|
|
if (nodeA.type === utils_1.AST_NODE_TYPES.CallExpression ||
|
|
nodeA.type === utils_1.AST_NODE_TYPES.Identifier ||
|
|
nodeA.type === utils_1.AST_NODE_TYPES.MemberExpression ||
|
|
nodeA.type === utils_1.AST_NODE_TYPES.MetaProperty) {
|
|
switch (nodeB.type) {
|
|
case utils_1.AST_NODE_TYPES.MemberExpression:
|
|
if (nodeB.property.type === utils_1.AST_NODE_TYPES.PrivateIdentifier) {
|
|
// Private identifiers in optional chaining is not currently allowed
|
|
// TODO - handle this once TS supports it (https://github.com/microsoft/TypeScript/issues/42734)
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
if (compareNodes(nodeA, nodeB.object) !== "Invalid" /* NodeComparisonResult.Invalid */) {
|
|
return "Subset" /* NodeComparisonResult.Subset */;
|
|
}
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
case utils_1.AST_NODE_TYPES.CallExpression:
|
|
if (compareNodes(nodeA, nodeB.callee) !== "Invalid" /* NodeComparisonResult.Invalid */) {
|
|
return "Subset" /* NodeComparisonResult.Subset */;
|
|
}
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
default:
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
}
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
switch (nodeA.type) {
|
|
// these expressions create a new instance each time - so it makes no sense to compare the chain
|
|
case utils_1.AST_NODE_TYPES.ArrayExpression:
|
|
case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
|
|
case utils_1.AST_NODE_TYPES.ClassExpression:
|
|
case utils_1.AST_NODE_TYPES.FunctionExpression:
|
|
case utils_1.AST_NODE_TYPES.JSXElement:
|
|
case utils_1.AST_NODE_TYPES.JSXFragment:
|
|
case utils_1.AST_NODE_TYPES.NewExpression:
|
|
case utils_1.AST_NODE_TYPES.ObjectExpression:
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
// chaining from assignments could change the value irrevocably - so it makes no sense to compare the chain
|
|
case utils_1.AST_NODE_TYPES.AssignmentExpression:
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
case utils_1.AST_NODE_TYPES.CallExpression: {
|
|
const nodeBCall = nodeB;
|
|
// check for cases like
|
|
// foo() && foo()(bar)
|
|
// ^^^^^ nodeA
|
|
// ^^^^^^^^^^ nodeB
|
|
// we don't want to check the arguments in this case
|
|
const aSubsetOfB = compareNodes(nodeA, nodeBCall.callee);
|
|
if (aSubsetOfB !== "Invalid" /* NodeComparisonResult.Invalid */) {
|
|
return "Subset" /* NodeComparisonResult.Subset */;
|
|
}
|
|
const calleeCompare = compareNodes(nodeA.callee, nodeBCall.callee);
|
|
if (calleeCompare !== "Equal" /* NodeComparisonResult.Equal */) {
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
// NOTE - we purposely ignore optional flag because for our purposes
|
|
// foo?.bar() && foo.bar?.()?.baz
|
|
// or
|
|
// foo.bar() && foo?.bar?.()?.baz
|
|
// are going to be exactly the same
|
|
const argumentCompare = compareArrays(nodeA.arguments, nodeBCall.arguments);
|
|
if (argumentCompare !== "Equal" /* NodeComparisonResult.Equal */) {
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
const typeParamCompare = compareNodes(nodeA.typeArguments, nodeBCall.typeArguments);
|
|
if (typeParamCompare === "Equal" /* NodeComparisonResult.Equal */) {
|
|
return "Equal" /* NodeComparisonResult.Equal */;
|
|
}
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
case utils_1.AST_NODE_TYPES.ChainExpression:
|
|
// special case handling for ChainExpression because it's allowed to be a subset
|
|
return compareNodes(nodeA, nodeB.expression);
|
|
case utils_1.AST_NODE_TYPES.Identifier:
|
|
case utils_1.AST_NODE_TYPES.PrivateIdentifier:
|
|
if (nodeA.name === nodeB.name) {
|
|
return "Equal" /* NodeComparisonResult.Equal */;
|
|
}
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
case utils_1.AST_NODE_TYPES.Literal: {
|
|
const nodeBLiteral = nodeB;
|
|
if (nodeA.raw === nodeBLiteral.raw &&
|
|
nodeA.value === nodeBLiteral.value) {
|
|
return "Equal" /* NodeComparisonResult.Equal */;
|
|
}
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
case utils_1.AST_NODE_TYPES.MemberExpression: {
|
|
const nodeBMember = nodeB;
|
|
if (nodeBMember.property.type === utils_1.AST_NODE_TYPES.PrivateIdentifier) {
|
|
// Private identifiers in optional chaining is not currently allowed
|
|
// TODO - handle this once TS supports it (https://github.com/microsoft/TypeScript/issues/42734)
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
// check for cases like
|
|
// foo.bar && foo.bar.baz
|
|
// ^^^^^^^ nodeA
|
|
// ^^^^^^^^^^^ nodeB
|
|
// result === Equal
|
|
//
|
|
// foo.bar && foo.bar.baz.bam
|
|
// ^^^^^^^ nodeA
|
|
// ^^^^^^^^^^^^^^^ nodeB
|
|
// result === Subset
|
|
//
|
|
// we don't want to check the property in this case
|
|
const aSubsetOfB = compareNodes(nodeA, nodeBMember.object);
|
|
if (aSubsetOfB !== "Invalid" /* NodeComparisonResult.Invalid */) {
|
|
return "Subset" /* NodeComparisonResult.Subset */;
|
|
}
|
|
if (nodeA.computed !== nodeBMember.computed) {
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
// NOTE - we purposely ignore optional flag because for our purposes
|
|
// foo?.bar && foo.bar?.baz
|
|
// or
|
|
// foo.bar && foo?.bar?.baz
|
|
// are going to be exactly the same
|
|
const objectCompare = compareNodes(nodeA.object, nodeBMember.object);
|
|
if (objectCompare !== "Equal" /* NodeComparisonResult.Equal */) {
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
return compareNodes(nodeA.property, nodeBMember.property);
|
|
}
|
|
case utils_1.AST_NODE_TYPES.TSTemplateLiteralType:
|
|
case utils_1.AST_NODE_TYPES.TemplateLiteral: {
|
|
const nodeBTemplate = nodeB;
|
|
const areQuasisEqual = nodeA.quasis.length === nodeBTemplate.quasis.length &&
|
|
nodeA.quasis.every((elA, idx) => {
|
|
const elB = nodeBTemplate.quasis[idx];
|
|
return elA.value.cooked === elB.value.cooked;
|
|
});
|
|
if (!areQuasisEqual) {
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
return "Equal" /* NodeComparisonResult.Equal */;
|
|
}
|
|
case utils_1.AST_NODE_TYPES.TemplateElement: {
|
|
const nodeBElement = nodeB;
|
|
if (nodeA.value.cooked === nodeBElement.value.cooked) {
|
|
return "Equal" /* NodeComparisonResult.Equal */;
|
|
}
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
// these aren't actually valid expressions.
|
|
// https://github.com/typescript-eslint/typescript-eslint/blob/20d7caee35ab84ae6381fdf04338c9e2b9e2bc48/packages/ast-spec/src/unions/Expression.ts#L37-L43
|
|
case utils_1.AST_NODE_TYPES.ArrayPattern:
|
|
case utils_1.AST_NODE_TYPES.ObjectPattern:
|
|
/* istanbul ignore next */
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
// update expression returns a number and also changes the value each time - so it makes no sense to compare the chain
|
|
case utils_1.AST_NODE_TYPES.UpdateExpression:
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
// yield returns the value passed to the `next` function, so it may not be the same each time - so it makes no sense to compare the chain
|
|
case utils_1.AST_NODE_TYPES.YieldExpression:
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
// general-case automatic handling of nodes to save us implementing every
|
|
// single case by hand. This just iterates the visitor keys to recursively
|
|
// check the children.
|
|
//
|
|
// Any specific logic cases or short-circuits should be listed as separate
|
|
// cases so that they don't fall into this generic handling
|
|
default:
|
|
return compareByVisiting(nodeA, nodeB);
|
|
}
|
|
}
|
|
const COMPARE_NODES_CACHE = new WeakMap();
|
|
/**
|
|
* Compares two nodes' ASTs to determine if the A is equal to or a subset of B
|
|
*/
|
|
function compareNodes(nodeA, nodeB) {
|
|
if (nodeA == null || nodeB == null) {
|
|
if (nodeA !== nodeB) {
|
|
return "Invalid" /* NodeComparisonResult.Invalid */;
|
|
}
|
|
return "Equal" /* NodeComparisonResult.Equal */;
|
|
}
|
|
const cached = COMPARE_NODES_CACHE.get(nodeA)?.get(nodeB);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
const result = compareNodesUncached(nodeA, nodeB);
|
|
let mapA = COMPARE_NODES_CACHE.get(nodeA);
|
|
if (mapA == null) {
|
|
mapA = new WeakMap();
|
|
COMPARE_NODES_CACHE.set(nodeA, mapA);
|
|
}
|
|
mapA.set(nodeB, result);
|
|
return result;
|
|
}
|
|
exports.compareNodes = compareNodes;
|
|
//# sourceMappingURL=compareNodes.js.map |