Files
2026-06-24 09:48:54 +02:00

428 lines
16 KiB
JavaScript

import { BindingUtils, CallExpressionUtils, } from "./shared-utils.js";
export class CollectionTracingUtils {
types;
bindingUtils;
callUtils;
visitedSetters = new Set();
visitedGetByIdSetters = new Set();
constructor(types) {
this.types = types;
this.bindingUtils = new BindingUtils(types);
this.callUtils = new CallExpressionUtils(types);
}
resetVisited() {
this.visitedSetters.clear();
this.visitedGetByIdSetters.clear();
}
traceCollectionSource(path) {
this.resetVisited();
return this.traceExpression(path);
}
traceExpression(path) {
if (path.isIdentifier()) {
return this.traceIdentifierToCollection(path);
}
if (path.isMemberExpression()) {
return this.traceMemberExpression(path);
}
if (path.isCallExpression()) {
return this.traceCallExpression(path);
}
if (path.isAwaitExpression()) {
const argument = path.get("argument");
if (argument.isExpression()) {
return this.traceExpression(argument);
}
}
if (path.isArrayExpression()) {
return this.traceArrayExpression(path);
}
return null;
}
traceIdentifierToCollection(path) {
const name = path.node.name;
const binding = path.scope.getBinding(name);
if (!binding)
return null;
if (binding.path.isVariableDeclarator()) {
return this.traceVariableDeclarator(binding.path, name);
}
if (this.bindingUtils.isFunctionParameter(name, path)) {
return this.traceParameterToCollection(name, path);
}
return null;
}
traceVariableDeclarator(declaratorPath, variableName) {
const init = declaratorPath.get("init");
if (init.isCallExpression()) {
const directResult = this.checkDirectServiceCall(init);
if (directResult)
return directResult;
const useStateInfo = this.bindingUtils.isUseStateCall(init);
if (useStateInfo?.setterName) {
return this.traceUseStateSetter(useStateInfo.setterName, declaratorPath);
}
}
if (init.isAwaitExpression()) {
const awaitArg = init.get("argument");
if (awaitArg.isCallExpression()) {
const directResult = this.checkDirectServiceCall(awaitArg);
if (directResult)
return directResult;
}
return this.traceExpression(init);
}
if (init.isConditionalExpression()) {
const consequent = init.get("consequent");
const alternate = init.get("alternate");
const result = this.traceExpression(consequent);
if (result)
return result;
return this.traceExpression(alternate);
}
if (init.isMemberExpression()) {
return this.traceExpression(init);
}
if (init.isIdentifier()) {
return this.traceExpression(init);
}
if (init.isCallExpression()) {
return this.traceExpression(init);
}
const id = declaratorPath.get("id");
if (id.isArrayPattern()) {
return this.traceArrayDestructuring(declaratorPath, id, variableName);
}
if (id.isObjectPattern()) {
return this.traceObjectDestructuring(declaratorPath, id, variableName);
}
return null;
}
traceUseStateSetter(setterName, declaratorPath) {
if (this.visitedSetters.has(setterName))
return null;
this.visitedSetters.add(setterName);
const functionScope = declaratorPath.getFunctionParent() ?? declaratorPath.scope.path;
// Pattern 1: setData(result) — direct call
const setterCall = this.bindingUtils.findSetterCallInScope(setterName, functionScope);
if (setterCall) {
const args = setterCall.get("arguments");
const firstArg = args[0];
if (!firstArg || !firstArg.isExpression())
return null;
return this.traceExpression(firstArg);
}
// Pattern 2: somePromise.then(setData) — setter passed as .then callback
const thenCall = this.findThenCallWithSetter(setterName, functionScope);
if (thenCall) {
const callee = thenCall.get("callee");
if (callee.isMemberExpression()) {
const promiseExpr = callee.get("object");
return this.traceExpression(promiseExpr);
}
}
return null;
}
findThenCallWithSetter(setterName, scope) {
let result = null;
scope.traverse({
CallExpression: (callPath) => {
if (result) {
callPath.stop();
return;
}
const callee = callPath.node.callee;
if (this.types.isMemberExpression(callee) &&
this.types.isIdentifier(callee.property) &&
callee.property.name === "then" &&
callPath.node.arguments.length > 0 &&
this.types.isIdentifier(callPath.node.arguments[0]) &&
callPath.node.arguments[0].name === setterName) {
result = callPath;
}
},
});
return result;
}
traceMemberExpression(path) {
const property = path.get("property");
const object = path.get("object");
if (property.isIdentifier() &&
property.node.name === "items") {
if (object.isExpression()) {
return this.traceExpression(object);
}
}
if (object.isIdentifier()) {
const objResult = this.traceIdentifierToCollection(object);
if (objResult && property.isIdentifier()) {
if (objResult.references.includes(property.node.name)) {
return { id: property.node.name, references: [] };
}
}
return objResult;
}
if (object.isExpression()) {
return this.traceExpression(object);
}
return null;
}
traceCallExpression(path) {
const directResult = this.checkDirectServiceCall(path);
if (directResult)
return directResult;
const useQueryResult = this.traceUseQueryCall(path);
if (useQueryResult)
return useQueryResult;
if (this.callUtils.isChainedArrayMethod(path.node)) {
const callee = path.get("callee");
if (callee.isMemberExpression()) {
const obj = callee.get("object");
return this.traceExpression(obj);
}
}
return null;
}
/**
* Trace useQuery({ queryFn: () => base44.entities.X.list() }) patterns.
* Extracts the collection from the queryFn return expression.
*/
traceUseQueryCall(path) {
const callee = path.get("callee");
if (!callee.isIdentifier() || callee.node.name !== "useQuery")
return null;
const args = path.get("arguments");
const configArg = args[0];
if (!configArg?.isObjectExpression())
return null;
for (const prop of configArg.get("properties")) {
if (!prop.isObjectProperty())
continue;
const key = prop.get("key");
if (!key.isIdentifier() || key.node.name !== "queryFn")
continue;
const value = prop.get("value");
if (value.isArrowFunctionExpression() || value.isFunctionExpression()) {
const fnBody = value.get("body");
if (fnBody.isCallExpression()) {
return this.checkDirectServiceCall(fnBody);
}
if (fnBody.isBlockStatement()) {
let result = null;
fnBody.traverse({
ReturnStatement: (retPath) => {
if (result)
return;
const arg = retPath.get("argument");
if (arg.isCallExpression()) {
result = this.checkDirectServiceCall(arg);
}
},
});
if (result)
return result;
}
}
}
return null;
}
checkDirectServiceCall(path) {
const getAllInfo = this.callUtils.isGetAllCall(path.node);
if (getAllInfo) {
return {
id: getAllInfo.collectionName,
references: getAllInfo.references,
};
}
const getByIdInfo = this.callUtils.isGetByIdCall(path.node);
if (getByIdInfo) {
return {
id: getByIdInfo.collectionName,
references: getByIdInfo.multiRefFields,
};
}
const base44List = this.callUtils.isBase44EntityListCall(path.node);
if (base44List) {
return { id: base44List.collectionName, references: [] };
}
const base44Get = this.callUtils.isBase44EntityGetCall(path.node);
if (base44Get) {
return { id: base44Get.collectionName, references: [] };
}
return null;
}
traceArrayDestructuring(declaratorPath, pattern, variableName) {
const elements = pattern.get("elements");
const index = elements.findIndex((el) => el.isIdentifier() && el.node.name === variableName);
if (index === -1)
return null;
const init = declaratorPath.get("init");
if (init.hasNode() && this.bindingUtils.isPromiseAllCall(init)) {
return this.tracePromiseAllElement(init, index);
}
if (init.isCallExpression()) {
const useStateInfo = this.bindingUtils.isUseStateCall(init);
if (useStateInfo && index === 0 && useStateInfo.setterName) {
return this.traceUseStateSetter(useStateInfo.setterName, declaratorPath);
}
}
return null;
}
traceObjectDestructuring(declaratorPath, pattern, variableName) {
const init = declaratorPath.get("init");
if (!init.isExpression())
return null;
const prop = pattern.node.properties.find((p) => this.types.isObjectProperty(p) &&
((this.types.isIdentifier(p.value) && p.value.name === variableName) ||
(this.types.isAssignmentPattern(p.value) &&
this.types.isIdentifier(p.value.left) &&
p.value.left.name === variableName)));
if (!prop || !this.types.isObjectProperty(prop))
return null;
const key = prop.key;
const propertyName = this.types.isIdentifier(key)
? key.name
: this.types.isStringLiteral(key)
? key.value
: null;
if (propertyName === "items") {
return this.traceExpression(init);
}
return this.traceExpression(init);
}
tracePromiseAllElement(init, index) {
let callExpr;
if (init.isAwaitExpression()) {
const arg = init.get("argument");
if (!arg.isCallExpression())
return null;
callExpr = arg;
}
else if (init.isCallExpression()) {
callExpr = init;
}
else {
return null;
}
const args = callExpr.get("arguments");
const firstArg = args[0];
if (!firstArg?.isArrayExpression())
return null;
const elements = firstArg.get("elements");
const targetElement = elements[index];
if (!targetElement || !targetElement.isExpression())
return null;
return this.traceExpression(targetElement);
}
traceParameterToCollection(paramName, path) {
const fn = path.getFunctionParent();
if (!fn)
return null;
const fnName = this.getFunctionName(fn);
if (!fnName)
return null;
const programPath = fn.findParent((p) => p.isProgram());
if (!programPath)
return null;
let result = null;
programPath.traverse({
JSXOpeningElement: (jsxPath) => {
if (result)
return;
const elementName = this.getJSXElementName(jsxPath.node);
if (elementName !== fnName)
return;
for (const attr of jsxPath.node.attributes) {
if (!this.types.isJSXAttribute(attr))
continue;
if (!this.types.isJSXIdentifier(attr.name))
continue;
if (attr.name.name !== paramName)
continue;
if (attr.value &&
this.types.isJSXExpressionContainer(attr.value) &&
this.types.isExpression(attr.value.expression)) {
const exprPath = jsxPath
.get("attributes")
.find((a) => a.isJSXAttribute() &&
this.types.isJSXIdentifier(a.node.name) &&
a.node.name.name === paramName);
if (exprPath && exprPath.isJSXAttribute()) {
const valPath = exprPath.get("value");
if (valPath.isJSXExpressionContainer()) {
const expr = valPath.get("expression");
if (expr.isExpression()) {
result = this.traceExpression(expr);
}
}
}
}
}
},
});
if (!result) {
return { id: paramName, references: [] };
}
return result;
}
traceArrayExpression(path) {
const elements = path.get("elements");
for (const el of elements) {
if (el.isSpreadElement()) {
const arg = el.get("argument");
if (arg.isExpression()) {
const result = this.traceExpression(arg);
if (result)
return result;
}
}
}
return null;
}
getFunctionName(fn) {
if (fn.isFunctionDeclaration() && fn.node.id) {
return fn.node.id.name;
}
const parent = fn.parentPath;
if (parent?.isVariableDeclarator()) {
const id = parent.get("id");
if (id.isIdentifier())
return id.node.name;
}
return null;
}
getJSXElementName(node) {
if (this.types.isJSXIdentifier(node.name)) {
return node.name.name;
}
return null;
}
traceGetByIdSource(path) {
this.visitedGetByIdSetters.clear();
return this.findGetByIdInScope(path);
}
findGetByIdInScope(path) {
const fn = path.getFunctionParent() ?? path.scope.path;
let result = null;
fn.traverse({
CallExpression: (callPath) => {
if (result)
return;
const info = this.callUtils.isGetByIdCall(callPath.node);
if (info) {
result = {
id: info.collectionName,
references: info.multiRefFields,
};
return;
}
const base44Get = this.callUtils.isBase44EntityGetCall(callPath.node);
if (base44Get) {
result = { id: base44Get.collectionName, references: [] };
}
},
});
return result;
}
}
//# sourceMappingURL=collection-tracing-utils.js.map