428 lines
16 KiB
JavaScript
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
|