73 lines
2.9 KiB
TypeScript
73 lines
2.9 KiB
TypeScript
export interface AstNode {
|
|
type: string;
|
|
start?: number;
|
|
loc?: { start?: { line?: number; column?: number } };
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export const nodeOf = (value: unknown): AstNode | null =>
|
|
value !== null && typeof value === "object" && typeof (value as { type?: unknown }).type === "string"
|
|
? (value as AstNode)
|
|
: null;
|
|
|
|
export const nodesOf = (value: unknown): AstNode[] =>
|
|
Array.isArray(value) ? value.map(nodeOf).filter((node): node is AstNode => node !== null) : [];
|
|
|
|
export const staticPropertyName = (node: AstNode | null): string | undefined => {
|
|
if (!node) return undefined;
|
|
if (node.type === "Literal" && (typeof node.value === "string" || typeof node.value === "number")) {
|
|
return String(node.value);
|
|
}
|
|
if (node.type === "BinaryExpression" && node.operator === "+") {
|
|
const left = staticPropertyName(nodeOf(node.left));
|
|
const right = staticPropertyName(nodeOf(node.right));
|
|
return left === undefined || right === undefined ? undefined : left + right;
|
|
}
|
|
if (node.type === "TemplateLiteral" && Array.isArray(node.expressions) && node.expressions.length === 0) {
|
|
return (node.quasis as Array<{ value?: { cooked?: unknown } }> | undefined)
|
|
?.map((quasi) => String(quasi.value?.cooked ?? ""))
|
|
.join("");
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
const callbackCollectionMethods = new Set(["every", "filter", "find", "findIndex", "map", "reduce", "some", "sort"]);
|
|
|
|
export function containsEscapingCallback(node: AstNode): boolean {
|
|
if (node.type === "ArrowFunctionExpression") return true;
|
|
if (node.type === "CallExpression") {
|
|
const callee = nodeOf(node.callee);
|
|
const arguments_ = nodesOf(node.arguments);
|
|
if (callee?.type === "ArrowFunctionExpression") {
|
|
if (arguments_.length > 0) return true;
|
|
const body = nodeOf(callee.body);
|
|
return body ? containsEscapingCallbackChildren(body) : false;
|
|
}
|
|
if (callee && containsEscapingCallback(callee)) return true;
|
|
const property = callee?.type === "MemberExpression" ? nodeOf(callee.property) : null;
|
|
const method = property?.type === "Identifier" ? String(property.name) : staticPropertyName(property);
|
|
return arguments_.some((argument) => {
|
|
if (argument.type !== "ArrowFunctionExpression" || !method || !callbackCollectionMethods.has(method)) {
|
|
return containsEscapingCallback(argument);
|
|
}
|
|
const body = nodeOf(argument.body);
|
|
return body ? containsEscapingCallback(body) : false;
|
|
});
|
|
}
|
|
return containsEscapingCallbackChildren(node);
|
|
}
|
|
|
|
function containsEscapingCallbackChildren(node: AstNode): boolean {
|
|
return Object.entries(node).some(([key, value]) => {
|
|
if (["loc", "start", "end", "type"].includes(key)) return false;
|
|
if (Array.isArray(value)) {
|
|
return value.some((entry) => {
|
|
const child = nodeOf(entry);
|
|
return child ? containsEscapingCallback(child) : false;
|
|
});
|
|
}
|
|
const child = nodeOf(value);
|
|
return child ? containsEscapingCallback(child) : false;
|
|
});
|
|
}
|