@saptools/service-flow 0.1.48 → 0.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +11 -1
- package/dist/{chunk-EGY2A4AT.js → chunk-52OUS3MO.js} +878 -333
- package/dist/chunk-52OUS3MO.js.map +1 -0
- package/dist/cli.js +330 -38
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/doctor.ts +187 -3
- package/src/db/migrations.ts +4 -1
- package/src/db/repositories.ts +188 -11
- package/src/db/schema.ts +1 -1
- package/src/linker/cross-repo-linker.ts +172 -6
- package/src/linker/operation-decorator-normalizer.ts +3 -3
- package/src/output/table-output.ts +18 -8
- package/src/parsers/decorator-parser.ts +128 -22
- package/src/parsers/imported-wrapper-parser.ts +20 -46
- package/src/parsers/operation-path-analysis.ts +350 -0
- package/src/parsers/outbound-call-parser.ts +63 -53
- package/src/trace/evidence.ts +18 -3
- package/src/trace/selectors.ts +36 -0
- package/src/trace/trace-engine.ts +99 -25
- package/src/types.ts +12 -0
- package/dist/chunk-EGY2A4AT.js.map +0 -1
|
@@ -315,6 +315,46 @@ import fs4 from "fs/promises";
|
|
|
315
315
|
import path5 from "path";
|
|
316
316
|
import ts2 from "typescript";
|
|
317
317
|
|
|
318
|
+
// src/linker/operation-decorator-normalizer.ts
|
|
319
|
+
function lowerFirst(value) {
|
|
320
|
+
return value ? `${value[0]?.toLowerCase() ?? ""}${value.slice(1)}` : value;
|
|
321
|
+
}
|
|
322
|
+
function normalizedOperationName(value) {
|
|
323
|
+
return value.replace(/^\//, "");
|
|
324
|
+
}
|
|
325
|
+
function clean(value) {
|
|
326
|
+
return value.replace(/^[`'"]|[`'"]$/g, "");
|
|
327
|
+
}
|
|
328
|
+
function generatedOperationNameFromConstant(value) {
|
|
329
|
+
for (const prefix of ["Action", "Func"]) {
|
|
330
|
+
if (value.startsWith(prefix) && value.length > prefix.length && /^[A-Z]/.test(value.slice(prefix.length))) return lowerFirst(value.slice(prefix.length));
|
|
331
|
+
}
|
|
332
|
+
return void 0;
|
|
333
|
+
}
|
|
334
|
+
function resolved(value, raw) {
|
|
335
|
+
const literal2 = clean(value);
|
|
336
|
+
const generated = generatedOperationNameFromConstant(literal2);
|
|
337
|
+
return { status: "resolved", operationName: generated ?? normalizedOperationName(literal2), raw };
|
|
338
|
+
}
|
|
339
|
+
function normalizeDecoratorOperationSignal(value, raw, candidateOperation) {
|
|
340
|
+
if (value) return resolved(value, raw);
|
|
341
|
+
if (!raw || raw.trim().length === 0) return { status: "none", raw };
|
|
342
|
+
const expression = raw.trim();
|
|
343
|
+
const nameMatch = /(?:^|\.)(Action[A-Z][\w$]*|Func[A-Z][\w$]*)\.name$/.exec(expression);
|
|
344
|
+
if (nameMatch?.[1]) return resolved(nameMatch[1], expression);
|
|
345
|
+
const stringMatch = /^String\(([A-Za-z_$][\w$]*)\)$/.exec(expression);
|
|
346
|
+
if (stringMatch?.[1]) {
|
|
347
|
+
const identifier = stringMatch[1];
|
|
348
|
+
const generated = generatedOperationNameFromConstant(identifier);
|
|
349
|
+
const normalizedCandidate2 = candidateOperation ? normalizedOperationName(candidateOperation) : void 0;
|
|
350
|
+
if (generated) return { status: "resolved", operationName: generated, raw: expression };
|
|
351
|
+
if (normalizedCandidate2 && identifier === normalizedCandidate2) return { status: "resolved", operationName: identifier, raw: expression };
|
|
352
|
+
return { status: "unsupported", raw: expression, reason: "string_wrapper_identifier_not_resolved" };
|
|
353
|
+
}
|
|
354
|
+
if (/^[`'"]/.test(expression) && !/[`'"]$/.test(expression)) return { status: "malformed", raw: expression, reason: "unterminated_literal" };
|
|
355
|
+
return { status: "unsupported", raw: expression, reason: "unsupported_decorator_expression" };
|
|
356
|
+
}
|
|
357
|
+
|
|
318
358
|
// src/parsers/ts-project.ts
|
|
319
359
|
import ts from "typescript";
|
|
320
360
|
function createSourceFile(filePath, text) {
|
|
@@ -340,16 +380,102 @@ function callName(d) {
|
|
|
340
380
|
}
|
|
341
381
|
function firstArg(d) {
|
|
342
382
|
const e = d.expression;
|
|
343
|
-
return ts2.isCallExpression(e)
|
|
383
|
+
return ts2.isCallExpression(e) ? e.arguments[0] : void 0;
|
|
384
|
+
}
|
|
385
|
+
function unwrapExpression(expression) {
|
|
386
|
+
let current = expression;
|
|
387
|
+
while (ts2.isParenthesizedExpression(current) || ts2.isAsExpression(current) || ts2.isTypeAssertionExpression(current) || ts2.isSatisfiesExpression(current)) current = current.expression;
|
|
388
|
+
return current;
|
|
389
|
+
}
|
|
390
|
+
function stringValue(expression) {
|
|
391
|
+
if (!expression) return void 0;
|
|
392
|
+
const unwrapped = unwrapExpression(expression);
|
|
393
|
+
return ts2.isStringLiteralLike(unwrapped) ? unwrapped.text : void 0;
|
|
394
|
+
}
|
|
395
|
+
function propertyName(node) {
|
|
396
|
+
if (ts2.isIdentifier(node) || ts2.isStringLiteralLike(node)) return node.text;
|
|
397
|
+
return void 0;
|
|
398
|
+
}
|
|
399
|
+
function collectEnumMembers(statement, lookups) {
|
|
400
|
+
for (const member of statement.members) {
|
|
401
|
+
const name = propertyName(member.name);
|
|
402
|
+
const value = stringValue(member.initializer);
|
|
403
|
+
if (name && value !== void 0)
|
|
404
|
+
lookups.enumMembers.set(`${statement.name.text}.${name}`, value);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function collectObjectProperties(name, initializer, lookups) {
|
|
408
|
+
const object = unwrapExpression(initializer);
|
|
409
|
+
if (!ts2.isObjectLiteralExpression(object)) return;
|
|
410
|
+
for (const property of object.properties) {
|
|
411
|
+
if (!ts2.isPropertyAssignment(property)) continue;
|
|
412
|
+
const key = propertyName(property.name);
|
|
413
|
+
const value = stringValue(property.initializer);
|
|
414
|
+
if (key && value !== void 0)
|
|
415
|
+
lookups.objectProperties.set(`${name}.${key}`, value);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function collectStringLookups(source) {
|
|
419
|
+
const lookups = {
|
|
420
|
+
identifiers: /* @__PURE__ */ new Map(),
|
|
421
|
+
enumMembers: /* @__PURE__ */ new Map(),
|
|
422
|
+
objectProperties: /* @__PURE__ */ new Map()
|
|
423
|
+
};
|
|
424
|
+
for (const statement of source.statements) {
|
|
425
|
+
if (ts2.isEnumDeclaration(statement)) collectEnumMembers(statement, lookups);
|
|
426
|
+
if (!ts2.isVariableStatement(statement) || !(statement.declarationList.flags & ts2.NodeFlags.Const)) continue;
|
|
427
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
428
|
+
if (!ts2.isIdentifier(declaration.name) || !declaration.initializer) continue;
|
|
429
|
+
const value = stringValue(declaration.initializer);
|
|
430
|
+
if (value !== void 0) lookups.identifiers.set(declaration.name.text, value);
|
|
431
|
+
collectObjectProperties(declaration.name.text, declaration.initializer, lookups);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return lookups;
|
|
435
|
+
}
|
|
436
|
+
function unresolved(rawExpression, reason) {
|
|
437
|
+
return { rawExpression, resolutionKind: "unresolved", unresolvedReason: reason };
|
|
438
|
+
}
|
|
439
|
+
function generatedConstant(rawExpression) {
|
|
440
|
+
const match = /(?:^|\.)(Action[A-Z][\w$]*|Func[A-Z][\w$]*)\.name$/.exec(rawExpression.trim());
|
|
441
|
+
return match?.[1] ? generatedOperationNameFromConstant(match[1]) : void 0;
|
|
442
|
+
}
|
|
443
|
+
function resolveDecoratorArgument(argument, lookups) {
|
|
444
|
+
if (!argument) return unresolved("", "decorator_argument_missing");
|
|
445
|
+
const rawExpression = argument.getText();
|
|
446
|
+
const expression = unwrapExpression(argument);
|
|
447
|
+
if (ts2.isStringLiteralLike(expression))
|
|
448
|
+
return { rawExpression, resolvedValue: expression.text, resolutionKind: "literal" };
|
|
449
|
+
if (ts2.isIdentifier(expression)) {
|
|
450
|
+
const value = lookups.identifiers.get(expression.text);
|
|
451
|
+
return value === void 0 ? unresolved(rawExpression, "identifier_not_resolved_to_local_const_string") : { rawExpression, resolvedValue: value, resolutionKind: "const_identifier" };
|
|
452
|
+
}
|
|
453
|
+
if (ts2.isPropertyAccessExpression(expression) && ts2.isIdentifier(expression.expression)) {
|
|
454
|
+
const key = `${expression.expression.text}.${expression.name.text}`;
|
|
455
|
+
const enumValue = lookups.enumMembers.get(key);
|
|
456
|
+
if (enumValue !== void 0)
|
|
457
|
+
return { rawExpression, resolvedValue: enumValue, resolutionKind: "enum_member" };
|
|
458
|
+
const objectValue2 = lookups.objectProperties.get(key);
|
|
459
|
+
if (objectValue2 !== void 0)
|
|
460
|
+
return { rawExpression, resolvedValue: objectValue2, resolutionKind: "const_object_property" };
|
|
461
|
+
}
|
|
462
|
+
const generatedValue = generatedConstant(rawExpression);
|
|
463
|
+
if (generatedValue !== void 0)
|
|
464
|
+
return {
|
|
465
|
+
rawExpression,
|
|
466
|
+
resolvedValue: generatedValue,
|
|
467
|
+
resolutionKind: "generated_constant_name"
|
|
468
|
+
};
|
|
469
|
+
if (ts2.isPropertyAccessExpression(expression))
|
|
470
|
+
return unresolved(rawExpression, "property_access_not_resolved_to_local_string");
|
|
471
|
+
return unresolved(rawExpression, "unsupported_decorator_expression");
|
|
344
472
|
}
|
|
345
473
|
async function parseDecorators(repoPath, filePath) {
|
|
346
474
|
const text = await fs4.readFile(path5.join(repoPath, filePath), "utf8");
|
|
347
475
|
const sf = createSourceFile(filePath, text);
|
|
348
|
-
const
|
|
476
|
+
const lookups = collectStringLookups(sf);
|
|
349
477
|
const handlers = [];
|
|
350
478
|
function visit(node) {
|
|
351
|
-
if (ts2.isVariableDeclaration(node) && ts2.isIdentifier(node.name) && node.initializer && ts2.isStringLiteralLike(node.initializer))
|
|
352
|
-
constants.set(node.name.text, node.initializer.text);
|
|
353
479
|
if (ts2.isClassDeclaration(node)) {
|
|
354
480
|
const className = node.name?.text ?? "AnonymousHandler";
|
|
355
481
|
const hasHandler = decs(node).some((d) => callName(d) === "Handler");
|
|
@@ -357,13 +483,13 @@ async function parseDecorators(repoPath, filePath) {
|
|
|
357
483
|
(m) => decs(m).filter(
|
|
358
484
|
(d) => ["Func", "Action", "On", "Event"].includes(callName(d))
|
|
359
485
|
).map((d) => {
|
|
360
|
-
const
|
|
361
|
-
const value = raw.startsWith('"') || raw.startsWith("'") || raw.startsWith("`") ? stripQuotes(raw) : constants.get(raw) ?? (raw.endsWith(".name") ? raw.split(".").at(-2) : void 0);
|
|
486
|
+
const decoratorResolution = resolveDecoratorArgument(firstArg(d), lookups);
|
|
362
487
|
return {
|
|
363
488
|
methodName: m.name.getText(),
|
|
364
489
|
decoratorKind: callName(d),
|
|
365
|
-
decoratorValue:
|
|
366
|
-
decoratorRawExpression:
|
|
490
|
+
decoratorValue: decoratorResolution.resolvedValue,
|
|
491
|
+
decoratorRawExpression: decoratorResolution.rawExpression,
|
|
492
|
+
decoratorResolution,
|
|
367
493
|
sourceFile: normalizePath(filePath),
|
|
368
494
|
sourceLine: line(sf, m.getStart())
|
|
369
495
|
};
|
|
@@ -588,7 +714,7 @@ import ts4 from "typescript";
|
|
|
588
714
|
function lineOf3(sf, node) {
|
|
589
715
|
return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
590
716
|
}
|
|
591
|
-
function
|
|
717
|
+
function stringValue2(node) {
|
|
592
718
|
if (!node) return void 0;
|
|
593
719
|
if (ts4.isStringLiteralLike(node) || ts4.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
594
720
|
if (ts4.isTemplateExpression(node)) return node.getText().replace(/^`|`$/g, "");
|
|
@@ -609,7 +735,7 @@ function connectFactFromCall(call) {
|
|
|
609
735
|
let alias;
|
|
610
736
|
let aliasExpr;
|
|
611
737
|
if (ts4.isStringLiteralLike(first) || ts4.isNoSubstitutionTemplateLiteral(first)) alias = first.text;
|
|
612
|
-
else if (!ts4.isObjectLiteralExpression(first)) aliasExpr =
|
|
738
|
+
else if (!ts4.isObjectLiteralExpression(first)) aliasExpr = stringValue2(first);
|
|
613
739
|
if ((ts4.isStringLiteralLike(first) || ts4.isNoSubstitutionTemplateLiteral(first)) && !objectArg) return { alias: first.text, isDynamic: false, placeholders: [] };
|
|
614
740
|
if (!objectArg && aliasExpr) return { aliasExpr, isDynamic: true, placeholders: placeholders(aliasExpr) };
|
|
615
741
|
const expressions = objectArg ? objectExpressions(objectArg) : {};
|
|
@@ -622,8 +748,8 @@ function objectExpressions(objectArg) {
|
|
|
622
748
|
for (const prop of obj.properties) {
|
|
623
749
|
if (!ts4.isPropertyAssignment(prop)) continue;
|
|
624
750
|
const name = ts4.isIdentifier(prop.name) || ts4.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
625
|
-
if (name === "destination") out.destinationExpr =
|
|
626
|
-
if (name === "path" || name === "servicePath") out.servicePathExpr =
|
|
751
|
+
if (name === "destination") out.destinationExpr = stringValue2(prop.initializer);
|
|
752
|
+
if (name === "path" || name === "servicePath") out.servicePathExpr = stringValue2(prop.initializer);
|
|
627
753
|
if (ts4.isObjectLiteralExpression(prop.initializer)) visitObject(prop.initializer);
|
|
628
754
|
}
|
|
629
755
|
}
|
|
@@ -733,8 +859,8 @@ function collectReturnedObjectBindings(fn) {
|
|
|
733
859
|
for (const prop of node.expression.properties) {
|
|
734
860
|
if (ts5.isShorthandPropertyAssignment(prop)) returns.set(prop.name.text, prop.name.text);
|
|
735
861
|
if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.initializer)) {
|
|
736
|
-
const
|
|
737
|
-
if (
|
|
862
|
+
const propertyName3 = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
863
|
+
if (propertyName3) returns.set(propertyName3, prop.initializer.text);
|
|
738
864
|
}
|
|
739
865
|
}
|
|
740
866
|
}
|
|
@@ -742,9 +868,9 @@ function collectReturnedObjectBindings(fn) {
|
|
|
742
868
|
}
|
|
743
869
|
visit(fn);
|
|
744
870
|
const out = /* @__PURE__ */ new Map();
|
|
745
|
-
for (const [
|
|
871
|
+
for (const [propertyName3, variableName] of returns) {
|
|
746
872
|
const fact = bindings.get(variableName);
|
|
747
|
-
if (fact) out.set(
|
|
873
|
+
if (fact) out.set(propertyName3, fact);
|
|
748
874
|
}
|
|
749
875
|
return out;
|
|
750
876
|
}
|
|
@@ -1028,8 +1154,8 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1028
1154
|
if (helpers.length === 0) return;
|
|
1029
1155
|
for (const el of decl.name.elements) {
|
|
1030
1156
|
if (!ts5.isIdentifier(el.name)) continue;
|
|
1031
|
-
const
|
|
1032
|
-
const matches2 = helpers.filter((row) => row.helper.returnedProperty ===
|
|
1157
|
+
const propertyName3 = el.propertyName && ts5.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
|
|
1158
|
+
const matches2 = helpers.filter((row) => row.helper.returnedProperty === propertyName3);
|
|
1033
1159
|
if (matches2.length !== 1) continue;
|
|
1034
1160
|
const resolved2 = matches2[0];
|
|
1035
1161
|
out.push({
|
|
@@ -1042,7 +1168,7 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1042
1168
|
placeholders: resolved2.helper.placeholders,
|
|
1043
1169
|
sourceFile: normalizePath(filePath),
|
|
1044
1170
|
sourceLine: lineOf3(sourceFileAst, decl),
|
|
1045
|
-
helperChain: [...resolved2.helper.helperChain ?? [], { callerVariable: el.name.text, helperFunction: call.expression.getText(sourceFileAst), returnedProperty:
|
|
1171
|
+
helperChain: [...resolved2.helper.helperChain ?? [], { callerVariable: el.name.text, helperFunction: call.expression.getText(sourceFileAst), returnedProperty: propertyName3, importSource: resolved2.imp?.sourceFile, exportedSymbol: resolved2.imp?.exportedName, helperSourceFile: resolved2.helper.sourceFile, helperSourceLine: resolved2.helper.sourceLine }]
|
|
1046
1172
|
});
|
|
1047
1173
|
}
|
|
1048
1174
|
}
|
|
@@ -1052,17 +1178,17 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1052
1178
|
const helpers = await helpersForCall(call);
|
|
1053
1179
|
if (helpers.length === 0) return;
|
|
1054
1180
|
for (const prop of pattern.properties) {
|
|
1055
|
-
let
|
|
1181
|
+
let propertyName3;
|
|
1056
1182
|
let targetName;
|
|
1057
1183
|
if (ts5.isShorthandPropertyAssignment(prop)) {
|
|
1058
|
-
|
|
1184
|
+
propertyName3 = prop.name.text;
|
|
1059
1185
|
targetName = prop.name.text;
|
|
1060
1186
|
} else if (ts5.isPropertyAssignment(prop)) {
|
|
1061
|
-
|
|
1187
|
+
propertyName3 = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
1062
1188
|
targetName = ts5.isIdentifier(prop.initializer) ? prop.initializer.text : void 0;
|
|
1063
1189
|
}
|
|
1064
|
-
if (!
|
|
1065
|
-
const matches2 = helpers.filter((row) => row.helper.returnedProperty ===
|
|
1190
|
+
if (!propertyName3 || !targetName) continue;
|
|
1191
|
+
const matches2 = helpers.filter((row) => row.helper.returnedProperty === propertyName3);
|
|
1066
1192
|
if (matches2.length !== 1) continue;
|
|
1067
1193
|
const resolved2 = matches2[0];
|
|
1068
1194
|
out.push({
|
|
@@ -1075,7 +1201,7 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1075
1201
|
placeholders: resolved2.helper.placeholders,
|
|
1076
1202
|
sourceFile: normalizePath(filePath),
|
|
1077
1203
|
sourceLine: lineOf3(sourceFileAst, node),
|
|
1078
|
-
helperChain: [...resolved2.helper.helperChain ?? [], { callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind: "assignment", scopeRule: "same-file-source-order", returnedProperty:
|
|
1204
|
+
helperChain: [...resolved2.helper.helperChain ?? [], { callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind: "assignment", scopeRule: "same-file-source-order", returnedProperty: propertyName3, importSource: resolved2.imp?.sourceFile, exportedSymbol: resolved2.imp?.exportedName, helperSourceFile: resolved2.helper.sourceFile, helperSourceLine: resolved2.helper.sourceLine }]
|
|
1079
1205
|
});
|
|
1080
1206
|
}
|
|
1081
1207
|
}
|
|
@@ -1087,9 +1213,9 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
1087
1213
|
if (target.expression.kind !== ts5.SyntaxKind.ThisKeyword) return;
|
|
1088
1214
|
for (const el of decl.name.elements) {
|
|
1089
1215
|
if (!ts5.isIdentifier(el.name)) continue;
|
|
1090
|
-
const
|
|
1216
|
+
const propertyName3 = el.propertyName && ts5.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
|
|
1091
1217
|
const helper = classHelpers.find(
|
|
1092
|
-
(h) => h.helperName === target.name.text && h.propertyName ===
|
|
1218
|
+
(h) => h.helperName === target.name.text && h.propertyName === propertyName3
|
|
1093
1219
|
);
|
|
1094
1220
|
if (!helper) continue;
|
|
1095
1221
|
out.push({
|
|
@@ -1242,13 +1368,13 @@ function collectClassHelpers(sf) {
|
|
|
1242
1368
|
});
|
|
1243
1369
|
}
|
|
1244
1370
|
if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.initializer)) {
|
|
1245
|
-
const
|
|
1246
|
-
const fact =
|
|
1247
|
-
if (
|
|
1371
|
+
const propertyName3 = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
1372
|
+
const fact = propertyName3 ? bindings.get(prop.initializer.text) : void 0;
|
|
1373
|
+
if (propertyName3 && fact)
|
|
1248
1374
|
helpers.push({
|
|
1249
1375
|
className,
|
|
1250
1376
|
helperName,
|
|
1251
|
-
propertyName:
|
|
1377
|
+
propertyName: propertyName3,
|
|
1252
1378
|
variableName: prop.initializer.text,
|
|
1253
1379
|
fact,
|
|
1254
1380
|
sourceLine: lineOf3(sf, prop)
|
|
@@ -1276,7 +1402,7 @@ function collectClassHelpers(sf) {
|
|
|
1276
1402
|
// src/parsers/outbound-call-parser.ts
|
|
1277
1403
|
import fs7 from "fs/promises";
|
|
1278
1404
|
import path10 from "path";
|
|
1279
|
-
import
|
|
1405
|
+
import ts8 from "typescript";
|
|
1280
1406
|
|
|
1281
1407
|
// src/linker/external-http-target.ts
|
|
1282
1408
|
import { createHash } from "crypto";
|
|
@@ -1552,7 +1678,242 @@ function topLevelQueryIndex(text) {
|
|
|
1552
1678
|
|
|
1553
1679
|
// src/parsers/imported-wrapper-parser.ts
|
|
1554
1680
|
import path9 from "path";
|
|
1681
|
+
import ts7 from "typescript";
|
|
1682
|
+
|
|
1683
|
+
// src/parsers/operation-path-analysis.ts
|
|
1555
1684
|
import ts6 from "typescript";
|
|
1685
|
+
var maxAliasDepth = 6;
|
|
1686
|
+
function analyzeOperationPath(expression, use, method = "POST") {
|
|
1687
|
+
if (!expression) return emptyAnalysis();
|
|
1688
|
+
const state = collectExpressionState(expression, use, 0, /* @__PURE__ */ new Set());
|
|
1689
|
+
const paths = unique(state.paths.map(normalizeRawPath));
|
|
1690
|
+
const normalized = unique(paths.flatMap((value) => normalizedCandidate(value, method)));
|
|
1691
|
+
const status = pathStatus(paths, state.placeholders, state.dynamic);
|
|
1692
|
+
const runtimeIdentifier = state.dynamic.at(-1)?.expression;
|
|
1693
|
+
return {
|
|
1694
|
+
status,
|
|
1695
|
+
rawExpression: expression.getText(expression.getSourceFile()),
|
|
1696
|
+
normalizedOperationPath: status === "static" && normalized.length === 1 ? normalized[0] : void 0,
|
|
1697
|
+
candidateRawPaths: paths,
|
|
1698
|
+
candidateNormalizedOperationPaths: normalized,
|
|
1699
|
+
placeholderKeys: unique([...state.placeholders, ...runtimeIdentifier ? [runtimeIdentifier] : []]),
|
|
1700
|
+
sourceKind: unique(state.sourceKinds).join("+") || "unknown",
|
|
1701
|
+
candidateIdentifier: ts6.isIdentifier(expression) ? expression.text : void 0,
|
|
1702
|
+
runtimeIdentifier,
|
|
1703
|
+
dynamicReassignments: state.dynamic,
|
|
1704
|
+
lexicalScope: lexicalEvidence(expression, use)
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
function operationPathExpression(analysis) {
|
|
1708
|
+
if (analysis.status === "ambiguous" || analysis.status === "unknown") return void 0;
|
|
1709
|
+
if (analysis.sourceKind.includes("parameter_binding")) return void 0;
|
|
1710
|
+
if (analysis.candidateRawPaths.length === 1 && analysis.dynamicReassignments.length === 0)
|
|
1711
|
+
return analysis.candidateRawPaths[0];
|
|
1712
|
+
if (!analysis.runtimeIdentifier || analysis.sourceKind.includes("binding_not_found"))
|
|
1713
|
+
return void 0;
|
|
1714
|
+
return isRuntimeIdentifier(analysis.runtimeIdentifier) ? `\${${analysis.runtimeIdentifier}}` : void 0;
|
|
1715
|
+
}
|
|
1716
|
+
function pathUnresolvedReason(analysis) {
|
|
1717
|
+
if (analysis.status === "ambiguous") return "ambiguous_operation_path_candidates";
|
|
1718
|
+
if (analysis.status === "dynamic" && !operationPathExpression(analysis))
|
|
1719
|
+
return "dynamic_operation_path_identifier";
|
|
1720
|
+
if (analysis.dynamicReassignments.length > 0) return "dynamic_operation_path_identifier";
|
|
1721
|
+
return void 0;
|
|
1722
|
+
}
|
|
1723
|
+
function collectExpressionState(expression, use, depth, seen) {
|
|
1724
|
+
const unwrapped = unwrap(expression);
|
|
1725
|
+
if (ts6.isStringLiteral(unwrapped)) return staticState(unwrapped.text, "string_literal");
|
|
1726
|
+
if (ts6.isNoSubstitutionTemplateLiteral(unwrapped))
|
|
1727
|
+
return staticState(unwrapped.text, "no_substitution_template");
|
|
1728
|
+
if (ts6.isTemplateExpression(unwrapped)) return templateState(unwrapped);
|
|
1729
|
+
if (ts6.isConditionalExpression(unwrapped))
|
|
1730
|
+
return mergeStates([
|
|
1731
|
+
collectExpressionState(unwrapped.whenTrue, use, depth + 1, seen),
|
|
1732
|
+
collectExpressionState(unwrapped.whenFalse, use, depth + 1, seen)
|
|
1733
|
+
], "conditional_candidates");
|
|
1734
|
+
if (ts6.isIdentifier(unwrapped))
|
|
1735
|
+
return collectIdentifierState(unwrapped, use, depth, seen);
|
|
1736
|
+
return dynamicState(unwrapped);
|
|
1737
|
+
}
|
|
1738
|
+
function collectIdentifierState(identifier, use, depth, seen) {
|
|
1739
|
+
if (depth >= maxAliasDepth)
|
|
1740
|
+
return dynamicState(identifier, "alias_depth_exceeded");
|
|
1741
|
+
const binding = resolveBinding(identifier, use);
|
|
1742
|
+
if (!binding || seen.has(binding.declaration))
|
|
1743
|
+
return dynamicState(identifier, binding ? "alias_cycle" : "binding_not_found");
|
|
1744
|
+
seen.add(binding.declaration);
|
|
1745
|
+
if (ts6.isParameter(binding.declaration))
|
|
1746
|
+
return dynamicState(identifier, "parameter_binding");
|
|
1747
|
+
const expressions = reachingExpressions(binding.declaration, use);
|
|
1748
|
+
if (expressions.length === 0) return dynamicState(identifier, "initializer_missing");
|
|
1749
|
+
const states = expressions.map((item) => collectExpressionState(item.expression, item.node, depth + 1, seen));
|
|
1750
|
+
const merged = mergeStates(states, binding.immutable ? "const_alias" : "mutable_alias");
|
|
1751
|
+
if (merged.dynamic.length === 0) return merged;
|
|
1752
|
+
return {
|
|
1753
|
+
...merged,
|
|
1754
|
+
dynamic: merged.dynamic.map((item) => item.expression === identifier.text ? item : item)
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
function reachingExpressions(declaration, use) {
|
|
1758
|
+
const rows2 = [];
|
|
1759
|
+
if (declaration.initializer) rows2.push({ expression: declaration.initializer, node: declaration });
|
|
1760
|
+
if ((declaration.parent.flags & ts6.NodeFlags.Const) !== 0) return rows2;
|
|
1761
|
+
const source = use.getSourceFile();
|
|
1762
|
+
const visit = (node) => {
|
|
1763
|
+
if (node.getStart(source) >= use.getStart(source)) return;
|
|
1764
|
+
if (node !== source && ts6.isFunctionLike(node) && !contains(node, use)) return;
|
|
1765
|
+
if (isAssignmentTo(node, declaration, use))
|
|
1766
|
+
rows2.push({ expression: node.right, node });
|
|
1767
|
+
ts6.forEachChild(node, visit);
|
|
1768
|
+
};
|
|
1769
|
+
visit(source);
|
|
1770
|
+
return rows2.sort((left, right) => left.node.getStart(source) - right.node.getStart(source));
|
|
1771
|
+
}
|
|
1772
|
+
function isAssignmentTo(node, declaration, use) {
|
|
1773
|
+
if (!ts6.isBinaryExpression(node) || node.operatorToken.kind !== ts6.SyntaxKind.EqualsToken)
|
|
1774
|
+
return false;
|
|
1775
|
+
if (!ts6.isIdentifier(node.left) || !ts6.isIdentifier(declaration.name)) return false;
|
|
1776
|
+
return resolveBinding(node.left, node)?.declaration === declaration && contains(declarationScope(declaration), use);
|
|
1777
|
+
}
|
|
1778
|
+
function resolveBinding(identifier, use) {
|
|
1779
|
+
const source = use.getSourceFile();
|
|
1780
|
+
const matches2 = [];
|
|
1781
|
+
const visit = (node) => {
|
|
1782
|
+
if (isNamedDeclaration(node, identifier.text) && isAccessible(node, use))
|
|
1783
|
+
matches2.push(node);
|
|
1784
|
+
ts6.forEachChild(node, visit);
|
|
1785
|
+
};
|
|
1786
|
+
visit(source);
|
|
1787
|
+
const declaration = matches2.sort((left, right) => right.getStart(source) - left.getStart(source))[0];
|
|
1788
|
+
if (!declaration) return void 0;
|
|
1789
|
+
const immutable = ts6.isVariableDeclaration(declaration) && (declaration.parent.flags & ts6.NodeFlags.Const) !== 0;
|
|
1790
|
+
return { declaration, immutable };
|
|
1791
|
+
}
|
|
1792
|
+
function isNamedDeclaration(node, name) {
|
|
1793
|
+
return (ts6.isVariableDeclaration(node) || ts6.isParameter(node)) && ts6.isIdentifier(node.name) && node.name.text === name;
|
|
1794
|
+
}
|
|
1795
|
+
function isAccessible(declaration, use) {
|
|
1796
|
+
const source = use.getSourceFile();
|
|
1797
|
+
if (declaration.name.getStart(source) >= use.getStart(source)) return false;
|
|
1798
|
+
const scope = declarationScope(declaration);
|
|
1799
|
+
return ts6.isSourceFile(scope) || contains(scope, use);
|
|
1800
|
+
}
|
|
1801
|
+
function declarationScope(declaration) {
|
|
1802
|
+
if (ts6.isParameter(declaration)) return declaration.parent;
|
|
1803
|
+
if (ts6.isCatchClause(declaration.parent)) return declaration.parent.block;
|
|
1804
|
+
const list = declaration.parent;
|
|
1805
|
+
if (isLoopInitializer(list.parent)) return list.parent.statement;
|
|
1806
|
+
const blockScoped = (list.flags & (ts6.NodeFlags.Const | ts6.NodeFlags.Let)) !== 0;
|
|
1807
|
+
let current = list.parent;
|
|
1808
|
+
if (!blockScoped) {
|
|
1809
|
+
while (current.parent && !ts6.isFunctionLike(current) && !ts6.isSourceFile(current))
|
|
1810
|
+
current = current.parent;
|
|
1811
|
+
return current;
|
|
1812
|
+
}
|
|
1813
|
+
while (current.parent && !isLexicalScope(current)) current = current.parent;
|
|
1814
|
+
return current;
|
|
1815
|
+
}
|
|
1816
|
+
function isLoopInitializer(node) {
|
|
1817
|
+
return ts6.isForStatement(node) || ts6.isForInStatement(node) || ts6.isForOfStatement(node);
|
|
1818
|
+
}
|
|
1819
|
+
function isLexicalScope(node) {
|
|
1820
|
+
return ts6.isBlock(node) || ts6.isSourceFile(node) || ts6.isModuleBlock(node) || ts6.isCaseBlock(node) || ts6.isFunctionLike(node);
|
|
1821
|
+
}
|
|
1822
|
+
function templateState(expression) {
|
|
1823
|
+
const value = expression.getText(expression.getSourceFile()).slice(1, -1);
|
|
1824
|
+
return {
|
|
1825
|
+
paths: [value],
|
|
1826
|
+
placeholders: expression.templateSpans.map((span) => span.expression.getText(expression.getSourceFile())),
|
|
1827
|
+
dynamic: [],
|
|
1828
|
+
sourceKinds: ["template_with_placeholders"]
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
function staticState(path11, sourceKind) {
|
|
1832
|
+
return { paths: [path11], placeholders: [], dynamic: [], sourceKinds: [sourceKind] };
|
|
1833
|
+
}
|
|
1834
|
+
function dynamicState(expression, sourceKind = "dynamic_expression") {
|
|
1835
|
+
return {
|
|
1836
|
+
paths: [],
|
|
1837
|
+
placeholders: [],
|
|
1838
|
+
dynamic: [{
|
|
1839
|
+
expression: expression.getText(expression.getSourceFile()),
|
|
1840
|
+
sourceLine: sourceLine(expression)
|
|
1841
|
+
}],
|
|
1842
|
+
sourceKinds: [sourceKind]
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
function mergeStates(states, sourceKind) {
|
|
1846
|
+
return {
|
|
1847
|
+
paths: states.flatMap((state) => state.paths),
|
|
1848
|
+
placeholders: states.flatMap((state) => state.placeholders),
|
|
1849
|
+
dynamic: states.flatMap((state) => state.dynamic),
|
|
1850
|
+
sourceKinds: [sourceKind, ...states.flatMap((state) => state.sourceKinds)]
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
function pathStatus(paths, placeholders3, dynamic) {
|
|
1854
|
+
if (dynamic.length > 0) return "dynamic";
|
|
1855
|
+
if (paths.length > 1) return "ambiguous";
|
|
1856
|
+
if (placeholders3.length > 0) return "dynamic";
|
|
1857
|
+
if (paths.length === 1) return "static";
|
|
1858
|
+
return "unknown";
|
|
1859
|
+
}
|
|
1860
|
+
function normalizedCandidate(value, method) {
|
|
1861
|
+
const invocation = normalizeODataOperationInvocationPath(value);
|
|
1862
|
+
if (invocation?.wasInvocation) return [invocation.normalizedOperationPath];
|
|
1863
|
+
const intent = classifyODataPathIntent(value, method);
|
|
1864
|
+
if (intent.kind.startsWith("entity_")) return [];
|
|
1865
|
+
if (!value.startsWith("/") || value.slice(1).includes("/") || value.includes("?")) return [];
|
|
1866
|
+
return [value];
|
|
1867
|
+
}
|
|
1868
|
+
function lexicalEvidence(expression, use) {
|
|
1869
|
+
if (!ts6.isIdentifier(expression))
|
|
1870
|
+
return { assignmentLines: [], sourceOrderSafe: expression.getStart() < use.getStart() };
|
|
1871
|
+
const binding = resolveBinding(expression, use);
|
|
1872
|
+
if (!binding) return { assignmentLines: [], sourceOrderSafe: false };
|
|
1873
|
+
const assignmentLines = ts6.isVariableDeclaration(binding.declaration) ? reachingExpressions(binding.declaration, use).slice(binding.declaration.initializer ? 1 : 0).map((item) => sourceLine(item.node)) : [];
|
|
1874
|
+
return {
|
|
1875
|
+
declarationLine: sourceLine(binding.declaration),
|
|
1876
|
+
assignmentLines,
|
|
1877
|
+
sourceOrderSafe: true
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
function unwrap(expression) {
|
|
1881
|
+
if (ts6.isAwaitExpression(expression) || ts6.isParenthesizedExpression(expression))
|
|
1882
|
+
return unwrap(expression.expression);
|
|
1883
|
+
if (ts6.isAsExpression(expression) || ts6.isSatisfiesExpression(expression) || ts6.isTypeAssertionExpression(expression))
|
|
1884
|
+
return unwrap(expression.expression);
|
|
1885
|
+
return expression;
|
|
1886
|
+
}
|
|
1887
|
+
function normalizeRawPath(value) {
|
|
1888
|
+
return value.startsWith("/") ? value : `/${value}`;
|
|
1889
|
+
}
|
|
1890
|
+
function isRuntimeIdentifier(value) {
|
|
1891
|
+
return /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(value);
|
|
1892
|
+
}
|
|
1893
|
+
function contains(parent, child) {
|
|
1894
|
+
const source = child.getSourceFile();
|
|
1895
|
+
return child.getStart(source) >= parent.getStart(source) && child.getEnd() <= parent.getEnd();
|
|
1896
|
+
}
|
|
1897
|
+
function sourceLine(node) {
|
|
1898
|
+
const source = node.getSourceFile();
|
|
1899
|
+
return source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
|
|
1900
|
+
}
|
|
1901
|
+
function unique(values) {
|
|
1902
|
+
return [...new Set(values)].sort();
|
|
1903
|
+
}
|
|
1904
|
+
function emptyAnalysis() {
|
|
1905
|
+
return {
|
|
1906
|
+
status: "unknown",
|
|
1907
|
+
candidateRawPaths: [],
|
|
1908
|
+
candidateNormalizedOperationPaths: [],
|
|
1909
|
+
placeholderKeys: [],
|
|
1910
|
+
sourceKind: "missing",
|
|
1911
|
+
dynamicReassignments: [],
|
|
1912
|
+
lexicalScope: { assignmentLines: [], sourceOrderSafe: false }
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// src/parsers/imported-wrapper-parser.ts
|
|
1556
1917
|
async function parseImportedWrapperCalls(repoPath, filePath, source, serviceBindings) {
|
|
1557
1918
|
const imports = await importsFor(repoPath, filePath, source);
|
|
1558
1919
|
const importedByLocal = new Map(imports.filter((item) => item.sourceFile).map((item) => [item.localName, item]));
|
|
@@ -1560,7 +1921,7 @@ async function parseImportedWrapperCalls(repoPath, filePath, source, serviceBind
|
|
|
1560
1921
|
const out = [];
|
|
1561
1922
|
const cache = /* @__PURE__ */ new Map();
|
|
1562
1923
|
for (const call of calls) {
|
|
1563
|
-
if (!
|
|
1924
|
+
if (!ts7.isIdentifier(call.expression)) continue;
|
|
1564
1925
|
const imported = importedByLocal.get(call.expression.text);
|
|
1565
1926
|
if (!imported?.sourceFile) continue;
|
|
1566
1927
|
const spec = await loadWrapperSpec(repoPath, imported, cache, 0);
|
|
@@ -1572,8 +1933,8 @@ async function parseImportedWrapperCalls(repoPath, filePath, source, serviceBind
|
|
|
1572
1933
|
function collectImportedCalls(source, imports) {
|
|
1573
1934
|
const calls = [];
|
|
1574
1935
|
const visit = (node) => {
|
|
1575
|
-
if (
|
|
1576
|
-
|
|
1936
|
+
if (ts7.isCallExpression(node) && ts7.isIdentifier(node.expression) && imports.has(node.expression.text)) calls.push(node);
|
|
1937
|
+
ts7.forEachChild(node, visit);
|
|
1577
1938
|
};
|
|
1578
1939
|
visit(source);
|
|
1579
1940
|
return calls;
|
|
@@ -1600,20 +1961,20 @@ function directSendSpec(source, sourceFile, name, fn) {
|
|
|
1600
1961
|
const params = parameterNames(fn);
|
|
1601
1962
|
const sends = [];
|
|
1602
1963
|
visitFunctionBody(fn, (node) => {
|
|
1603
|
-
if (
|
|
1964
|
+
if (ts7.isCallExpression(node) && ts7.isPropertyAccessExpression(node.expression) && node.expression.name.text === "send") sends.push(node);
|
|
1604
1965
|
});
|
|
1605
1966
|
if (sends.length !== 1) return void 0;
|
|
1606
1967
|
const send = sends[0];
|
|
1607
|
-
const receiver = send &&
|
|
1968
|
+
const receiver = send && ts7.isPropertyAccessExpression(send.expression) && ts7.isIdentifier(send.expression.expression) ? send.expression.expression.text : void 0;
|
|
1608
1969
|
const object = send?.arguments[0];
|
|
1609
|
-
if (!receiver || !object || !
|
|
1970
|
+
if (!receiver || !object || !ts7.isObjectLiteralExpression(object)) return void 0;
|
|
1610
1971
|
const pathExpr = propertyExpression(object, "path");
|
|
1611
1972
|
const methodExpr = propertyExpression(object, "method");
|
|
1612
|
-
const pathName = pathExpr &&
|
|
1973
|
+
const pathName = pathExpr && ts7.isIdentifier(pathExpr) ? pathExpr.text : void 0;
|
|
1613
1974
|
const clientIndex = params.indexOf(receiver);
|
|
1614
1975
|
const pathIndex = pathName ? params.indexOf(pathName) : -1;
|
|
1615
1976
|
if (clientIndex < 0 || pathIndex < 0) return void 0;
|
|
1616
|
-
const methodName = methodExpr &&
|
|
1977
|
+
const methodName = methodExpr && ts7.isIdentifier(methodExpr) ? methodExpr.text : void 0;
|
|
1617
1978
|
const methodIndex = methodName ? params.indexOf(methodName) : -1;
|
|
1618
1979
|
return { clientIndex, pathIndex, methodIndex: methodIndex >= 0 ? methodIndex : void 0, methodLiteral: literal(methodExpr), sourceFile, sourceLine: lineOf3(source, fn), chain: [name] };
|
|
1619
1980
|
}
|
|
@@ -1622,11 +1983,11 @@ async function nestedSendSpec(repoPath, sourceFile, source, name, fn, cache, dep
|
|
|
1622
1983
|
const byLocal = new Map(imports.filter((item) => item.sourceFile).map((item) => [item.localName, item]));
|
|
1623
1984
|
const calls = [];
|
|
1624
1985
|
visitFunctionBody(fn, (node) => {
|
|
1625
|
-
if (
|
|
1986
|
+
if (ts7.isCallExpression(node) && ts7.isIdentifier(node.expression) && byLocal.has(node.expression.text)) calls.push(node);
|
|
1626
1987
|
});
|
|
1627
1988
|
if (calls.length !== 1) return void 0;
|
|
1628
1989
|
const call = calls[0];
|
|
1629
|
-
const imported = call &&
|
|
1990
|
+
const imported = call && ts7.isIdentifier(call.expression) ? byLocal.get(call.expression.text) : void 0;
|
|
1630
1991
|
const nested = imported ? await loadWrapperSpec(repoPath, imported, cache, depth + 1) : void 0;
|
|
1631
1992
|
if (!call || !nested) return void 0;
|
|
1632
1993
|
const params = parameterNames(fn);
|
|
@@ -1638,12 +1999,12 @@ async function nestedSendSpec(repoPath, sourceFile, source, name, fn, cache, dep
|
|
|
1638
1999
|
}
|
|
1639
2000
|
function wrapperCallFact(source, filePath, call, spec, serviceBindings) {
|
|
1640
2001
|
const client = call.arguments[spec.clientIndex];
|
|
1641
|
-
if (!client || !
|
|
1642
|
-
const pathValue = resolveCallerPath(call.arguments[spec.pathIndex], call);
|
|
2002
|
+
if (!client || !ts7.isIdentifier(client) || !serviceBindings.has(client.text)) return void 0;
|
|
1643
2003
|
const methodValue = spec.methodIndex === void 0 ? spec.methodLiteral : literal(call.arguments[spec.methodIndex]);
|
|
1644
2004
|
const method = (methodValue ?? "POST").toUpperCase();
|
|
1645
|
-
const
|
|
1646
|
-
const operationPathExpr =
|
|
2005
|
+
const pathAnalysis = analyzeOperationPath(call.arguments[spec.pathIndex], call, method);
|
|
2006
|
+
const operationPathExpr = operationPathExpression(pathAnalysis);
|
|
2007
|
+
const unresolvedReason = pathUnresolvedReason(pathAnalysis);
|
|
1647
2008
|
return {
|
|
1648
2009
|
callType: "remote_action",
|
|
1649
2010
|
serviceVariableName: client.text,
|
|
@@ -1653,59 +2014,41 @@ function wrapperCallFact(source, filePath, call, spec, serviceBindings) {
|
|
|
1653
2014
|
sourceFile: normalizePath(filePath),
|
|
1654
2015
|
sourceLine: lineOf3(source, call),
|
|
1655
2016
|
confidence: operationPathExpr ? 0.85 : 0.5,
|
|
1656
|
-
unresolvedReason
|
|
2017
|
+
unresolvedReason,
|
|
1657
2018
|
evidence: {
|
|
1658
2019
|
parser: "typescript_ast",
|
|
1659
|
-
classifier:
|
|
2020
|
+
classifier: importedWrapperClassifier(pathAnalysis.status),
|
|
1660
2021
|
receiver: client.text,
|
|
1661
2022
|
wrapperFunction: spec.chain[0],
|
|
1662
2023
|
wrapperChain: spec.chain,
|
|
1663
2024
|
callerSite: { sourceFile: normalizePath(filePath), sourceLine: lineOf3(source, call) },
|
|
1664
2025
|
calleeSite: { sourceFile: spec.sourceFile, sourceLine: spec.sourceLine },
|
|
1665
|
-
rawPathExpression:
|
|
1666
|
-
missingPathIdentifier:
|
|
1667
|
-
|
|
2026
|
+
rawPathExpression: pathAnalysis.rawExpression,
|
|
2027
|
+
missingPathIdentifier: pathAnalysis.runtimeIdentifier,
|
|
2028
|
+
pathAnalysis,
|
|
2029
|
+
odataPathIntent: operationPathExpr ? classifyODataPathIntent(operationPathExpr, method) : void 0
|
|
1668
2030
|
}
|
|
1669
2031
|
};
|
|
1670
2032
|
}
|
|
1671
|
-
function
|
|
1672
|
-
if (
|
|
1673
|
-
if (
|
|
1674
|
-
|
|
1675
|
-
if (ts6.isIdentifier(expr)) {
|
|
1676
|
-
const initializer = constInitializer(expr.text, use);
|
|
1677
|
-
if (initializer) {
|
|
1678
|
-
const resolved2 = resolveCallerPath(initializer, initializer);
|
|
1679
|
-
return { ...resolved2, sourceKind: "const", rawExpression: expr.text };
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
return { sourceKind: "dynamic", rawExpression: expr.getText() };
|
|
1683
|
-
}
|
|
1684
|
-
function constInitializer(name, use) {
|
|
1685
|
-
let found;
|
|
1686
|
-
const source = use.getSourceFile();
|
|
1687
|
-
const visit = (node) => {
|
|
1688
|
-
if (node.getStart(source) >= use.getStart(source)) return;
|
|
1689
|
-
if (ts6.isVariableDeclaration(node) && ts6.isIdentifier(node.name) && node.name.text === name && node.initializer && (node.parent.flags & ts6.NodeFlags.Const) !== 0) found = node.initializer;
|
|
1690
|
-
ts6.forEachChild(node, visit);
|
|
1691
|
-
};
|
|
1692
|
-
visit(source);
|
|
1693
|
-
return found;
|
|
2033
|
+
function importedWrapperClassifier(status) {
|
|
2034
|
+
if (status === "static") return "imported_wrapper_literal_path";
|
|
2035
|
+
if (status === "ambiguous") return "imported_wrapper_ambiguous_path";
|
|
2036
|
+
return "imported_wrapper_dynamic_path";
|
|
1694
2037
|
}
|
|
1695
2038
|
function findFunction(source, exportedName) {
|
|
1696
2039
|
const localName = exportedLocalName(source, exportedName);
|
|
1697
2040
|
for (const statement of source.statements) {
|
|
1698
|
-
if (
|
|
1699
|
-
if (!
|
|
2041
|
+
if (ts7.isFunctionDeclaration(statement) && statement.name?.text === localName) return { name: exportedName, fn: statement };
|
|
2042
|
+
if (!ts7.isVariableStatement(statement)) continue;
|
|
1700
2043
|
for (const declaration of statement.declarationList.declarations) {
|
|
1701
|
-
if (
|
|
2044
|
+
if (ts7.isIdentifier(declaration.name) && declaration.name.text === localName && declaration.initializer && (ts7.isArrowFunction(declaration.initializer) || ts7.isFunctionExpression(declaration.initializer))) return { name: exportedName, fn: declaration.initializer };
|
|
1702
2045
|
}
|
|
1703
2046
|
}
|
|
1704
2047
|
return void 0;
|
|
1705
2048
|
}
|
|
1706
2049
|
function exportedLocalName(source, exportedName) {
|
|
1707
2050
|
for (const statement of source.statements) {
|
|
1708
|
-
if (!
|
|
2051
|
+
if (!ts7.isExportDeclaration(statement) || !statement.exportClause || !ts7.isNamedExports(statement.exportClause)) continue;
|
|
1709
2052
|
const match = statement.exportClause.elements.find((item) => item.name.text === exportedName);
|
|
1710
2053
|
if (match) return match.propertyName?.text ?? match.name.text;
|
|
1711
2054
|
}
|
|
@@ -1713,36 +2056,30 @@ function exportedLocalName(source, exportedName) {
|
|
|
1713
2056
|
}
|
|
1714
2057
|
function visitFunctionBody(fn, visitor) {
|
|
1715
2058
|
const visit = (node) => {
|
|
1716
|
-
if (node !== fn &&
|
|
2059
|
+
if (node !== fn && ts7.isFunctionLike(node)) return;
|
|
1717
2060
|
visitor(node);
|
|
1718
|
-
|
|
2061
|
+
ts7.forEachChild(node, visit);
|
|
1719
2062
|
};
|
|
1720
2063
|
if (fn.body) visit(fn.body);
|
|
1721
2064
|
}
|
|
1722
2065
|
function parameterNames(fn) {
|
|
1723
|
-
return fn.parameters.map((parameter) =>
|
|
2066
|
+
return fn.parameters.map((parameter) => ts7.isIdentifier(parameter.name) ? parameter.name.text : "");
|
|
1724
2067
|
}
|
|
1725
2068
|
function mappedParameterIndex(expr, parameters) {
|
|
1726
|
-
return expr &&
|
|
2069
|
+
return expr && ts7.isIdentifier(expr) ? parameters.indexOf(expr.text) : -1;
|
|
1727
2070
|
}
|
|
1728
2071
|
function propertyExpression(object, key) {
|
|
1729
2072
|
for (const property of object.properties) {
|
|
1730
|
-
if (
|
|
1731
|
-
if (
|
|
2073
|
+
if (ts7.isPropertyAssignment(property) && propertyName2(property.name) === key) return property.initializer;
|
|
2074
|
+
if (ts7.isShorthandPropertyAssignment(property) && property.name.text === key) return property.name;
|
|
1732
2075
|
}
|
|
1733
2076
|
return void 0;
|
|
1734
2077
|
}
|
|
1735
|
-
function
|
|
1736
|
-
return
|
|
2078
|
+
function propertyName2(name) {
|
|
2079
|
+
return ts7.isIdentifier(name) || ts7.isStringLiteralLike(name) ? name.text : void 0;
|
|
1737
2080
|
}
|
|
1738
2081
|
function literal(expr) {
|
|
1739
|
-
return expr && (
|
|
1740
|
-
}
|
|
1741
|
-
function normalizeOperationPath(value) {
|
|
1742
|
-
return value.startsWith("/") ? value : `/${value}`;
|
|
1743
|
-
}
|
|
1744
|
-
function runtimePathExpression(value) {
|
|
1745
|
-
return value ? `\${${value}}` : void 0;
|
|
2082
|
+
return expr && (ts7.isStringLiteralLike(expr) || ts7.isNoSubstitutionTemplateLiteral(expr)) ? expr.text : void 0;
|
|
1746
2083
|
}
|
|
1747
2084
|
|
|
1748
2085
|
// src/parsers/outbound-call-parser.ts
|
|
@@ -1751,47 +2088,47 @@ function lineOf4(text, idx) {
|
|
|
1751
2088
|
}
|
|
1752
2089
|
function entityFromExpression(expr) {
|
|
1753
2090
|
if (!expr) return void 0;
|
|
1754
|
-
if (
|
|
1755
|
-
if (
|
|
1756
|
-
if (
|
|
2091
|
+
if (ts8.isIdentifier(expr) || ts8.isStringLiteral(expr) || ts8.isNoSubstitutionTemplateLiteral(expr)) return expr.text;
|
|
2092
|
+
if (ts8.isPropertyAccessExpression(expr) && expr.expression.kind === ts8.SyntaxKind.ThisKeyword) return expr.name.text;
|
|
2093
|
+
if (ts8.isElementAccessExpression(expr) && expr.argumentExpression && (ts8.isStringLiteral(expr.argumentExpression) || ts8.isNoSubstitutionTemplateLiteral(expr.argumentExpression))) return expr.argumentExpression.text;
|
|
1757
2094
|
return void 0;
|
|
1758
2095
|
}
|
|
1759
2096
|
function expressionName(expr) {
|
|
1760
|
-
if (
|
|
1761
|
-
if (
|
|
2097
|
+
if (ts8.isIdentifier(expr)) return expr.text;
|
|
2098
|
+
if (ts8.isPropertyAccessExpression(expr)) return `${expressionName(expr.expression)}.${expr.name.text}`;
|
|
1762
2099
|
return expr.getText();
|
|
1763
2100
|
}
|
|
1764
2101
|
function variableInitializers(source) {
|
|
1765
2102
|
const initializers = /* @__PURE__ */ new Map();
|
|
1766
2103
|
for (const statement of source.statements) {
|
|
1767
|
-
if (!
|
|
2104
|
+
if (!ts8.isVariableStatement(statement) || (statement.declarationList.flags & ts8.NodeFlags.Const) === 0) continue;
|
|
1768
2105
|
for (const declaration of statement.declarationList.declarations) {
|
|
1769
|
-
if (
|
|
2106
|
+
if (ts8.isIdentifier(declaration.name) && declaration.initializer) initializers.set(declaration.name.text, declaration.initializer);
|
|
1770
2107
|
}
|
|
1771
2108
|
}
|
|
1772
2109
|
return initializers;
|
|
1773
2110
|
}
|
|
1774
2111
|
function queryEntityFromAst(expr, initializers = /* @__PURE__ */ new Map()) {
|
|
1775
|
-
if (
|
|
1776
|
-
if (
|
|
1777
|
-
if (
|
|
2112
|
+
if (ts8.isParenthesizedExpression(expr) || ts8.isAwaitExpression(expr)) return queryEntityFromAst(expr.expression, initializers);
|
|
2113
|
+
if (ts8.isIdentifier(expr) && initializers.has(expr.text)) return queryEntityFromAst(initializers.get(expr.text), initializers);
|
|
2114
|
+
if (ts8.isCallExpression(expr)) {
|
|
1778
2115
|
const name = expressionName(expr.expression);
|
|
1779
2116
|
if (name === "cds.run") return queryEntityFromAst(expr.arguments[0], initializers);
|
|
1780
2117
|
if (["SELECT.one.from", "SELECT.from", "SELECT.one", "INSERT.into", "UPSERT.into", "DELETE.from", "UPDATE.entity"].includes(name)) return entityFromExpression(expr.arguments[0]);
|
|
1781
2118
|
if (name === "UPDATE") return entityFromExpression(expr.arguments[0]);
|
|
1782
|
-
const receiver =
|
|
2119
|
+
const receiver = ts8.isPropertyAccessExpression(expr.expression) ? expr.expression.expression : void 0;
|
|
1783
2120
|
if (receiver) return queryEntityFromAst(receiver, initializers);
|
|
1784
2121
|
}
|
|
1785
2122
|
return void 0;
|
|
1786
2123
|
}
|
|
1787
2124
|
function extractQueryEntity(expr) {
|
|
1788
|
-
const source =
|
|
2125
|
+
const source = ts8.createSourceFile("query.ts", `const __query = (${expr});`, ts8.ScriptTarget.Latest, true, ts8.ScriptKind.TS);
|
|
1789
2126
|
const initializers = variableInitializers(source);
|
|
1790
2127
|
let found;
|
|
1791
2128
|
const visit = (node) => {
|
|
1792
2129
|
if (found) return;
|
|
1793
|
-
if (
|
|
1794
|
-
|
|
2130
|
+
if (ts8.isParenthesizedExpression(node)) found = queryEntityFromAst(node.expression, initializers);
|
|
2131
|
+
ts8.forEachChild(node, visit);
|
|
1795
2132
|
};
|
|
1796
2133
|
visit(source);
|
|
1797
2134
|
return found;
|
|
@@ -1805,7 +2142,7 @@ function parserEvidence(source, node, extra) {
|
|
|
1805
2142
|
return { parser: "typescript_ast", startOffset: node.getStart(source), endOffset: node.getEnd(), ...extra };
|
|
1806
2143
|
}
|
|
1807
2144
|
function isStringLike(expr) {
|
|
1808
|
-
return Boolean(expr && (
|
|
2145
|
+
return Boolean(expr && (ts8.isStringLiteral(expr) || ts8.isNoSubstitutionTemplateLiteral(expr)));
|
|
1809
2146
|
}
|
|
1810
2147
|
function literalText(expr) {
|
|
1811
2148
|
if (isStringLike(expr)) return expr.text;
|
|
@@ -1813,151 +2150,118 @@ function literalText(expr) {
|
|
|
1813
2150
|
}
|
|
1814
2151
|
function objectPropertyText(object, key) {
|
|
1815
2152
|
const prop = object.properties.find(
|
|
1816
|
-
(property) =>
|
|
2153
|
+
(property) => ts8.isPropertyAssignment(property) && nameOfProperty(property.name) === key || ts8.isShorthandPropertyAssignment(property) && property.name.text === key
|
|
1817
2154
|
);
|
|
1818
2155
|
if (!prop) return void 0;
|
|
1819
|
-
return
|
|
2156
|
+
return ts8.isShorthandPropertyAssignment(prop) ? prop.name.text : prop.initializer.getText();
|
|
1820
2157
|
}
|
|
1821
2158
|
function objectPropertyIsShorthand(object, key) {
|
|
1822
|
-
return object.properties.some((property) =>
|
|
2159
|
+
return object.properties.some((property) => ts8.isShorthandPropertyAssignment(property) && property.name.text === key);
|
|
1823
2160
|
}
|
|
1824
2161
|
function nameOfProperty(name) {
|
|
1825
|
-
if (
|
|
2162
|
+
if (ts8.isIdentifier(name) || ts8.isStringLiteral(name) || ts8.isNumericLiteral(name)) return name.text;
|
|
1826
2163
|
return void 0;
|
|
1827
2164
|
}
|
|
1828
|
-
var
|
|
2165
|
+
var maxAliasDepth2 = 5;
|
|
1829
2166
|
function safeRaw(expr) {
|
|
1830
|
-
if (
|
|
2167
|
+
if (ts8.isStringLiteral(expr) || ts8.isNoSubstitutionTemplateLiteral(expr) || ts8.isIdentifier(expr) || ts8.isTemplateExpression(expr)) return expr.getText(expr.getSourceFile());
|
|
1831
2168
|
return void 0;
|
|
1832
2169
|
}
|
|
1833
2170
|
function placeholders2(expr) {
|
|
1834
2171
|
return expr.templateSpans.map((span) => span.expression.getText(expr.getSourceFile()));
|
|
1835
2172
|
}
|
|
1836
2173
|
function isFunctionLikeScope(node) {
|
|
1837
|
-
return
|
|
2174
|
+
return ts8.isFunctionLike(node) || ts8.isSourceFile(node);
|
|
1838
2175
|
}
|
|
1839
2176
|
function nodeContains(parent, child) {
|
|
1840
2177
|
const source = child.getSourceFile();
|
|
1841
2178
|
return child.getStart(source) >= parent.getStart(source) && child.getEnd() <= parent.getEnd();
|
|
1842
2179
|
}
|
|
1843
|
-
function
|
|
1844
|
-
if (
|
|
1845
|
-
if (
|
|
2180
|
+
function declarationScope2(node) {
|
|
2181
|
+
if (ts8.isParameter(node)) return node.parent;
|
|
2182
|
+
if (ts8.isCatchClause(node.parent) && node.parent.variableDeclaration === node) return node.parent;
|
|
1846
2183
|
const list = node.parent;
|
|
1847
|
-
const blockScoped = (list.flags & (
|
|
2184
|
+
const blockScoped = (list.flags & (ts8.NodeFlags.Const | ts8.NodeFlags.Let)) !== 0;
|
|
1848
2185
|
let current = list.parent;
|
|
1849
2186
|
if (!blockScoped) {
|
|
1850
2187
|
while (current.parent && !isFunctionLikeScope(current)) current = current.parent;
|
|
1851
2188
|
return current;
|
|
1852
2189
|
}
|
|
1853
|
-
while (current.parent && !
|
|
2190
|
+
while (current.parent && !ts8.isBlock(current) && !ts8.isSourceFile(current) && !ts8.isModuleBlock(current) && !ts8.isCaseBlock(current) && !isLoopInitializerScope(node, current) && !isFunctionLikeScope(current)) current = current.parent;
|
|
1854
2191
|
return current;
|
|
1855
2192
|
}
|
|
1856
2193
|
function isLoopInitializerScope(declaration, scope) {
|
|
1857
2194
|
const list = declaration.parent;
|
|
1858
|
-
return
|
|
2195
|
+
return ts8.isForStatement(scope) && scope.initializer === list || (ts8.isForInStatement(scope) || ts8.isForOfStatement(scope)) && scope.initializer === list;
|
|
1859
2196
|
}
|
|
1860
2197
|
function catchBindingScope(declaration) {
|
|
1861
|
-
if (
|
|
1862
|
-
return
|
|
2198
|
+
if (ts8.isParameter(declaration)) return void 0;
|
|
2199
|
+
return ts8.isCatchClause(declaration.parent) && declaration.parent.variableDeclaration === declaration ? declaration.parent : void 0;
|
|
1863
2200
|
}
|
|
1864
2201
|
function isAccessibleDeclaration(declaration, use) {
|
|
1865
2202
|
const source = use.getSourceFile();
|
|
1866
2203
|
if (declaration.name.getStart(source) >= use.getStart(source)) return false;
|
|
1867
2204
|
const catchScope = catchBindingScope(declaration);
|
|
1868
2205
|
if (catchScope) return nodeContains(catchScope.block, use);
|
|
1869
|
-
const scope =
|
|
1870
|
-
if (
|
|
1871
|
-
return
|
|
2206
|
+
const scope = declarationScope2(declaration);
|
|
2207
|
+
if (ts8.isForStatement(scope) || ts8.isForInStatement(scope) || ts8.isForOfStatement(scope)) return nodeContains(scope.statement, use);
|
|
2208
|
+
return ts8.isSourceFile(scope) || nodeContains(scope, use);
|
|
1872
2209
|
}
|
|
1873
|
-
function
|
|
2210
|
+
function resolveBinding2(identifier, use) {
|
|
1874
2211
|
const source = use.getSourceFile();
|
|
1875
2212
|
let best;
|
|
1876
2213
|
const visit = (node) => {
|
|
1877
|
-
if (
|
|
1878
|
-
if (
|
|
1879
|
-
|
|
2214
|
+
if (ts8.isVariableDeclaration(node) && ts8.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
|
|
2215
|
+
if (ts8.isParameter(node) && ts8.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
|
|
2216
|
+
ts8.forEachChild(node, visit);
|
|
1880
2217
|
};
|
|
1881
2218
|
visit(source);
|
|
1882
2219
|
if (!best) return { immutable: false, evidence: ["binding_not_found"] };
|
|
1883
|
-
const immutable =
|
|
1884
|
-
return { declaration: best, initializer:
|
|
2220
|
+
const immutable = ts8.isVariableDeclaration(best) && (best.parent.flags & ts8.NodeFlags.Const) !== 0;
|
|
2221
|
+
return { declaration: best, initializer: ts8.isVariableDeclaration(best) ? best.initializer : void 0, immutable, evidence: [immutable ? "lexical_const_binding_before_use" : "lexical_mutable_or_parameter_binding"] };
|
|
1885
2222
|
}
|
|
1886
2223
|
function resolveExpression(expr, use, policy, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
1887
2224
|
if (!expr) return { status: "unknown", sourceKind: "dynamic_expression", placeholderKeys: [], evidence: ["expression_missing"] };
|
|
1888
|
-
if (
|
|
1889
|
-
if (
|
|
1890
|
-
if (
|
|
2225
|
+
if (ts8.isStringLiteral(expr)) return { status: "static", sourceKind: "string_literal", value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["string_literal"] };
|
|
2226
|
+
if (ts8.isNoSubstitutionTemplateLiteral(expr)) return { status: "static", sourceKind: "no_substitution_template", value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["no_substitution_template"] };
|
|
2227
|
+
if (ts8.isTemplateExpression(expr)) {
|
|
1891
2228
|
const keys = placeholders2(expr);
|
|
1892
2229
|
if (policy === "operation_path") return { status: "dynamic", sourceKind: "template_with_substitutions", value: stripQuotes(expr.getText(expr.getSourceFile())), rawExpression: safeRaw(expr), placeholderKeys: keys, evidence: ["operation_path_template_placeholders_retained"] };
|
|
1893
2230
|
return { status: "dynamic", sourceKind: "template_with_substitutions", placeholderKeys: keys, evidence: ["template_substitutions_not_static_external_target"] };
|
|
1894
2231
|
}
|
|
1895
|
-
if (
|
|
1896
|
-
if (depth >=
|
|
1897
|
-
const binding =
|
|
2232
|
+
if (ts8.isIdentifier(expr)) {
|
|
2233
|
+
if (depth >= maxAliasDepth2) return { status: "unknown", sourceKind: "const_alias", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["alias_depth_exceeded"], constName: expr.text };
|
|
2234
|
+
const binding = resolveBinding2(expr, use);
|
|
1898
2235
|
if (!binding.declaration || !binding.initializer || !binding.immutable) return { status: "dynamic", sourceKind: "dynamic_expression", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: binding.evidence, constName: expr.text };
|
|
1899
2236
|
if (seen.has(binding.declaration)) return { status: "unknown", sourceKind: "const_alias", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["alias_cycle_detected"], constName: expr.text };
|
|
1900
2237
|
seen.add(binding.declaration);
|
|
1901
2238
|
const resolved2 = resolveExpression(binding.initializer, binding.declaration, policy, depth + 1, seen);
|
|
1902
2239
|
return { ...resolved2, sourceKind: "const_alias", rawExpression: safeRaw(expr), constName: expr.text, evidence: [...binding.evidence, ...resolved2.evidence] };
|
|
1903
2240
|
}
|
|
1904
|
-
return { status: "dynamic", sourceKind: "dynamic_expression", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: [`unsupported_${
|
|
2241
|
+
return { status: "dynamic", sourceKind: "dynamic_expression", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: [`unsupported_${ts8.SyntaxKind[expr.kind] ?? "expression"}`] };
|
|
1905
2242
|
}
|
|
1906
2243
|
function staticExpressionText(expr, initializers) {
|
|
1907
2244
|
if (!expr) return void 0;
|
|
1908
2245
|
if (isStringLike(expr)) return expr.text;
|
|
1909
|
-
if (
|
|
2246
|
+
if (ts8.isIdentifier(expr) && initializers.has(expr.text)) return staticExpressionText(initializers.get(expr.text), initializers);
|
|
1910
2247
|
return void 0;
|
|
1911
2248
|
}
|
|
1912
|
-
function staticPathExpression(expr, use) {
|
|
1913
|
-
const resolution = resolveExpression(expr, use, "operation_path");
|
|
1914
|
-
const sourceKind = resolution.sourceKind === "const_alias" ? "const" : resolution.sourceKind === "template_with_substitutions" || resolution.sourceKind === "no_substitution_template" ? "template" : resolution.status === "static" ? "literal" : "dynamic";
|
|
1915
|
-
return { text: resolution.value, sourceKind, rawExpression: resolution.rawExpression, constName: resolution.constName, resolution };
|
|
1916
|
-
}
|
|
1917
2249
|
function operationPathFromStatic(text) {
|
|
1918
2250
|
return text ? `/${stripQuotes(text).replace(/^\//, "")}` : void 0;
|
|
1919
2251
|
}
|
|
1920
|
-
function staticPathCandidates(identifier, call, method) {
|
|
1921
|
-
const binding = resolveBinding(identifier, call);
|
|
1922
|
-
const declaration = binding.declaration;
|
|
1923
|
-
if (!declaration) return void 0;
|
|
1924
|
-
const source = call.getSourceFile();
|
|
1925
|
-
const paths = [];
|
|
1926
|
-
let hasDynamicAssignments = false;
|
|
1927
|
-
const addExpr = (expr, origin) => {
|
|
1928
|
-
const resolved2 = staticPathExpression(expr, origin);
|
|
1929
|
-
if (resolved2.text) paths.push(operationPathFromStatic(resolved2.text) ?? resolved2.text);
|
|
1930
|
-
else hasDynamicAssignments = true;
|
|
1931
|
-
};
|
|
1932
|
-
if (ts7.isVariableDeclaration(declaration)) addExpr(declaration.initializer, declaration);
|
|
1933
|
-
const visit = (node) => {
|
|
1934
|
-
if (node !== source && isFunctionLikeScope(node) && !nodeContains(node, call)) return;
|
|
1935
|
-
if (node.getStart(source) >= call.getStart(source)) return;
|
|
1936
|
-
if (ts7.isBinaryExpression(node) && node.operatorToken.kind === ts7.SyntaxKind.EqualsToken && ts7.isIdentifier(node.left)) {
|
|
1937
|
-
const target = resolveBinding(node.left, node);
|
|
1938
|
-
if (target.declaration === declaration) addExpr(node.right, node);
|
|
1939
|
-
}
|
|
1940
|
-
ts7.forEachChild(node, visit);
|
|
1941
|
-
};
|
|
1942
|
-
visit(source);
|
|
1943
|
-
const candidatePaths = [...new Set(paths)];
|
|
1944
|
-
if (candidatePaths.length === 0) return void 0;
|
|
1945
|
-
const normalizedCandidateOperations = [...new Set(candidatePaths.map((candidate) => classifyODataPathIntent(candidate, method).topLevelOperationName).filter((value) => Boolean(value)))];
|
|
1946
|
-
return { candidatePaths, normalizedCandidateOperations, candidateSourceKind: "static candidates observed before call in lexical scope", candidateIdentifier: identifier.text, hasDynamicAssignments, conservativeReason: hasDynamicAssignments ? "dynamic_assignment_observed" : normalizedCandidateOperations.length > 1 ? "candidate_tie" : void 0 };
|
|
1947
|
-
}
|
|
1948
2252
|
function destinationExpressionShape(expr) {
|
|
1949
2253
|
if (!expr) return void 0;
|
|
1950
|
-
if (
|
|
1951
|
-
if (
|
|
1952
|
-
if (
|
|
1953
|
-
if (
|
|
1954
|
-
if (
|
|
1955
|
-
if (
|
|
1956
|
-
return
|
|
2254
|
+
if (ts8.isIdentifier(expr)) return "identifier";
|
|
2255
|
+
if (ts8.isPropertyAccessExpression(expr) || ts8.isElementAccessExpression(expr)) return "property_read";
|
|
2256
|
+
if (ts8.isCallExpression(expr)) return "function_call";
|
|
2257
|
+
if (ts8.isConditionalExpression(expr)) return "conditional";
|
|
2258
|
+
if (ts8.isBinaryExpression(expr)) return "binary_expression";
|
|
2259
|
+
if (ts8.isTemplateExpression(expr)) return "template_expression";
|
|
2260
|
+
return ts8.SyntaxKind[expr.kind] ?? "expression";
|
|
1957
2261
|
}
|
|
1958
2262
|
function staticConditionalCandidates(expr, initializers) {
|
|
1959
|
-
const resolved2 = expr &&
|
|
1960
|
-
if (!resolved2 || !
|
|
2263
|
+
const resolved2 = expr && ts8.isIdentifier(expr) && initializers.has(expr.text) ? initializers.get(expr.text) : expr;
|
|
2264
|
+
if (!resolved2 || !ts8.isConditionalExpression(resolved2)) return void 0;
|
|
1961
2265
|
const left = staticExpressionText(resolved2.whenTrue, initializers);
|
|
1962
2266
|
const right = staticExpressionText(resolved2.whenFalse, initializers);
|
|
1963
2267
|
if (!left || !right) return void 0;
|
|
@@ -1965,8 +2269,8 @@ function staticConditionalCandidates(expr, initializers) {
|
|
|
1965
2269
|
}
|
|
1966
2270
|
function propertyInitializer(object, key) {
|
|
1967
2271
|
for (const property of object.properties) {
|
|
1968
|
-
if (
|
|
1969
|
-
if (
|
|
2272
|
+
if (ts8.isPropertyAssignment(property) && nameOfProperty(property.name) === key) return property.initializer;
|
|
2273
|
+
if (ts8.isShorthandPropertyAssignment(property) && property.name.text === key) return property.name;
|
|
1970
2274
|
}
|
|
1971
2275
|
return void 0;
|
|
1972
2276
|
}
|
|
@@ -1979,6 +2283,30 @@ function safeOperationName(value) {
|
|
|
1979
2283
|
if (!value || !/^[A-Za-z_$][\w$]*(?:[./][A-Za-z_$][\w$]*)*$/.test(value)) return void 0;
|
|
1980
2284
|
return operationPathFromStatic(value);
|
|
1981
2285
|
}
|
|
2286
|
+
function wrapperSourceKind(sourceKind) {
|
|
2287
|
+
if (sourceKind.includes("const_alias")) return "const";
|
|
2288
|
+
if (sourceKind.includes("template")) return "template";
|
|
2289
|
+
if (sourceKind.includes("string_literal")) return "literal";
|
|
2290
|
+
return sourceKind.includes("conditional") ? "ambiguous" : "dynamic";
|
|
2291
|
+
}
|
|
2292
|
+
function literalPathSource(analysis) {
|
|
2293
|
+
if (analysis.status !== "static") return void 0;
|
|
2294
|
+
if (analysis.sourceKind.includes("const_alias")) return "same_scope_const_initializer";
|
|
2295
|
+
if (analysis.sourceKind.includes("no_substitution_template")) return "template";
|
|
2296
|
+
return analysis.sourceKind.includes("string_literal") ? "literal" : analysis.sourceKind;
|
|
2297
|
+
}
|
|
2298
|
+
function legacyPathCandidates(analysis) {
|
|
2299
|
+
if (analysis.candidateRawPaths.length < 2 && analysis.dynamicReassignments.length === 0)
|
|
2300
|
+
return void 0;
|
|
2301
|
+
return {
|
|
2302
|
+
candidatePaths: analysis.candidateRawPaths,
|
|
2303
|
+
normalizedCandidateOperations: analysis.candidateNormalizedOperationPaths.map((value) => value.replace(/^\//, "")),
|
|
2304
|
+
candidateSourceKind: analysis.sourceKind,
|
|
2305
|
+
candidateIdentifier: analysis.candidateIdentifier,
|
|
2306
|
+
hasDynamicAssignments: analysis.dynamicReassignments.length > 0,
|
|
2307
|
+
conservativeReason: analysis.dynamicReassignments.length > 0 ? "dynamic_assignment_observed" : "candidate_tie"
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
1982
2310
|
function hasTemplatePlaceholder(value) {
|
|
1983
2311
|
return /\$\{|%7B|%7D/i.test(value);
|
|
1984
2312
|
}
|
|
@@ -2003,7 +2331,7 @@ function externalHttpEvidence(node, source) {
|
|
|
2003
2331
|
const exprText = expr.getText(source);
|
|
2004
2332
|
if (exprText === "useOrFetchDestination") {
|
|
2005
2333
|
const objectArg = node.arguments[0];
|
|
2006
|
-
if (objectArg &&
|
|
2334
|
+
if (objectArg && ts8.isObjectLiteralExpression(objectArg)) {
|
|
2007
2335
|
const destination = destinationTargetFromExpression(propertyInitializer(objectArg, "destinationName"), node);
|
|
2008
2336
|
return { externalTarget: destination ?? { kind: "unknown", dynamic: false }, classifier: "sap_destination_lookup", sourceCallShape: "useOrFetchDestination" };
|
|
2009
2337
|
}
|
|
@@ -2011,13 +2339,13 @@ function externalHttpEvidence(node, source) {
|
|
|
2011
2339
|
if (exprText === "executeHttpRequest") {
|
|
2012
2340
|
const destination = destinationTargetFromExpression(node.arguments[0], node);
|
|
2013
2341
|
const config = node.arguments[1];
|
|
2014
|
-
const method = config &&
|
|
2015
|
-
const url = config &&
|
|
2342
|
+
const method = config && ts8.isObjectLiteralExpression(config) ? httpMethodFromObject(config, node) : void 0;
|
|
2343
|
+
const url = config && ts8.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, "url"), node) : { kind: "unknown", dynamic: false };
|
|
2016
2344
|
return { method, externalTarget: destination ? { ...url, destination } : url, classifier: "sap_execute_http_request", sourceCallShape: "executeHttpRequest" };
|
|
2017
2345
|
}
|
|
2018
2346
|
if (exprText === "axios") {
|
|
2019
2347
|
const config = node.arguments[0];
|
|
2020
|
-
if (config &&
|
|
2348
|
+
if (config && ts8.isObjectLiteralExpression(config)) {
|
|
2021
2349
|
const method = httpMethodFromObject(config, node);
|
|
2022
2350
|
return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, "url"), node), classifier: "axios_config_call", sourceCallShape: "axios(config)" };
|
|
2023
2351
|
}
|
|
@@ -2025,10 +2353,10 @@ function externalHttpEvidence(node, source) {
|
|
|
2025
2353
|
}
|
|
2026
2354
|
if (exprText === "fetch") {
|
|
2027
2355
|
const init = node.arguments[1];
|
|
2028
|
-
const method = init &&
|
|
2356
|
+
const method = init && ts8.isObjectLiteralExpression(init) ? httpMethodFromObject(init, node) : void 0;
|
|
2029
2357
|
return { method, externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: "fetch_call", sourceCallShape: "fetch" };
|
|
2030
2358
|
}
|
|
2031
|
-
if (
|
|
2359
|
+
if (ts8.isPropertyAccessExpression(expr) && ["get", "post", "put", "patch", "delete", "head"].includes(expr.name.text) && expr.expression.getText(source) === "axios") {
|
|
2032
2360
|
return { method: expr.name.text.toUpperCase(), externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: "axios_member_call", sourceCallShape: `axios.${expr.name.text}` };
|
|
2033
2361
|
}
|
|
2034
2362
|
return void 0;
|
|
@@ -2036,27 +2364,27 @@ function externalHttpEvidence(node, source) {
|
|
|
2036
2364
|
function collectServiceVariables(source) {
|
|
2037
2365
|
const vars = /* @__PURE__ */ new Set(["cds", "messaging", "messageClient", "eventClient"]);
|
|
2038
2366
|
const visit = (node) => {
|
|
2039
|
-
if (
|
|
2367
|
+
if (ts8.isVariableDeclaration(node) && ts8.isIdentifier(node.name) && node.initializer) {
|
|
2040
2368
|
const text = node.initializer.getText(source);
|
|
2041
2369
|
if (/cds\.connect\.(to|messaging)\s*\(/.test(text)) vars.add(node.name.text);
|
|
2042
2370
|
}
|
|
2043
|
-
|
|
2371
|
+
ts8.forEachChild(node, visit);
|
|
2044
2372
|
};
|
|
2045
2373
|
visit(source);
|
|
2046
2374
|
return vars;
|
|
2047
2375
|
}
|
|
2048
2376
|
function receiverName(expr) {
|
|
2049
|
-
if (
|
|
2050
|
-
if (
|
|
2377
|
+
if (ts8.isIdentifier(expr)) return expr.text;
|
|
2378
|
+
if (ts8.isPropertyAccessExpression(expr)) return expr.getText(sourceOf(expr));
|
|
2051
2379
|
return void 0;
|
|
2052
2380
|
}
|
|
2053
2381
|
function sourceOf(node) {
|
|
2054
2382
|
return node.getSourceFile();
|
|
2055
2383
|
}
|
|
2056
2384
|
function rootReceiverName(expr) {
|
|
2057
|
-
if (
|
|
2058
|
-
if (
|
|
2059
|
-
if (
|
|
2385
|
+
if (ts8.isIdentifier(expr)) return expr.text;
|
|
2386
|
+
if (ts8.isPropertyAccessExpression(expr)) return rootReceiverName(expr.expression);
|
|
2387
|
+
if (ts8.isCallExpression(expr)) return rootReceiverName(expr.expression);
|
|
2060
2388
|
return void 0;
|
|
2061
2389
|
}
|
|
2062
2390
|
function isSupportedEventReceiver(receiver, rootReceiver, serviceVariables) {
|
|
@@ -2073,38 +2401,38 @@ function collectWrapperSpecs(source) {
|
|
|
2073
2401
|
const serviceVariables = collectServiceVariables(source);
|
|
2074
2402
|
const calledNames = /* @__PURE__ */ new Set();
|
|
2075
2403
|
const collectCalls = (node) => {
|
|
2076
|
-
if (
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2404
|
+
if (ts8.isCallExpression(node) && ts8.isIdentifier(node.expression))
|
|
2405
|
+
calledNames.add(node.expression.text);
|
|
2406
|
+
if (ts8.isCallExpression(node) && ts8.isCallExpression(node.expression) && ts8.isIdentifier(node.expression.expression))
|
|
2407
|
+
calledNames.add(node.expression.expression.text);
|
|
2408
|
+
ts8.forEachChild(node, collectCalls);
|
|
2081
2409
|
};
|
|
2082
2410
|
collectCalls(source);
|
|
2083
2411
|
const scanFunction = (name, fn) => {
|
|
2084
|
-
if (!calledNames.has(name)) return;
|
|
2085
|
-
const params = fn.parameters.map((param) =>
|
|
2412
|
+
if (!calledNames.has(name) && !isExportedWrapper(fn)) return;
|
|
2413
|
+
const params = fn.parameters.map((param) => ts8.isIdentifier(param.name) ? param.name.text : void 0);
|
|
2086
2414
|
const sends = [];
|
|
2087
2415
|
const visit = (node) => {
|
|
2088
|
-
if (
|
|
2416
|
+
if (ts8.isCallExpression(node) && ts8.isPropertyAccessExpression(node.expression) && node.expression.name.text === "send" && ts8.isIdentifier(node.expression.expression)) {
|
|
2089
2417
|
const objectArg = node.arguments[0];
|
|
2090
|
-
if (objectArg &&
|
|
2418
|
+
if (objectArg && ts8.isObjectLiteralExpression(objectArg)) {
|
|
2091
2419
|
const pathProp = propertyInitializer(objectArg, "path");
|
|
2092
2420
|
const methodProp = propertyInitializer(objectArg, "method");
|
|
2093
|
-
const pathName = pathProp &&
|
|
2094
|
-
const methodName = methodProp &&
|
|
2421
|
+
const pathName = pathProp && ts8.isIdentifier(pathProp) ? pathProp.text : void 0;
|
|
2422
|
+
const methodName = methodProp && ts8.isIdentifier(methodProp) ? methodProp.text : void 0;
|
|
2095
2423
|
const methodLiteral = resolveExpression(methodProp, node, "literal").value;
|
|
2096
2424
|
if (pathName) sends.push({ client: node.expression.expression.text, path: pathName, method: methodName, methodLiteral, start: node.getStart(source), end: node.getEnd() });
|
|
2097
2425
|
}
|
|
2098
2426
|
}
|
|
2099
|
-
if (
|
|
2427
|
+
if (ts8.isCallExpression(node) && ts8.isIdentifier(node.expression) && specs.has(node.expression.text)) {
|
|
2100
2428
|
const nested = specs.get(node.expression.text);
|
|
2101
2429
|
const pathArg = nested ? node.arguments[nested.pathIndex] : void 0;
|
|
2102
2430
|
const clientArg = nested?.clientIndex === void 0 ? void 0 : node.arguments[nested.clientIndex];
|
|
2103
|
-
const pathName = pathArg &&
|
|
2104
|
-
const clientName = clientArg &&
|
|
2431
|
+
const pathName = pathArg && ts8.isIdentifier(pathArg) ? pathArg.text : void 0;
|
|
2432
|
+
const clientName = clientArg && ts8.isIdentifier(clientArg) ? clientArg.text : nested?.clientName;
|
|
2105
2433
|
if (nested && pathName && clientName) sends.push({ client: clientName, path: pathName, method: nested.methodName, methodLiteral: nested.methodLiteral, nestedWrapperFunction: node.expression.text, start: node.getStart(source), end: node.getEnd() });
|
|
2106
2434
|
}
|
|
2107
|
-
|
|
2435
|
+
ts8.forEachChild(node, visit);
|
|
2108
2436
|
};
|
|
2109
2437
|
visit(fn);
|
|
2110
2438
|
if (sends.length !== 1) return;
|
|
@@ -2116,13 +2444,18 @@ function collectWrapperSpecs(source) {
|
|
|
2116
2444
|
if (pathIndex >= 0 && (clientIndex >= 0 || capturesKnownClient)) specs.set(name, { clientIndex: clientIndex >= 0 ? clientIndex : void 0, clientName: clientIndex >= 0 ? void 0 : found.client, pathIndex, methodIndex: methodIndex >= 0 ? methodIndex : void 0, methodName: found.method, methodLiteral: found.methodLiteral, nestedWrapperFunction: found.nestedWrapperFunction, definitionLine: lineOf4(source.text, fn.getStart(source)), internalStart: found.start, internalEnd: found.end });
|
|
2117
2445
|
};
|
|
2118
2446
|
const visitTop = (node) => {
|
|
2119
|
-
if (
|
|
2120
|
-
if (
|
|
2121
|
-
|
|
2447
|
+
if (ts8.isFunctionDeclaration(node) && node.name) scanFunction(node.name.text, node);
|
|
2448
|
+
if (ts8.isVariableDeclaration(node) && ts8.isIdentifier(node.name) && node.initializer && (ts8.isArrowFunction(node.initializer) || ts8.isFunctionExpression(node.initializer))) scanFunction(node.name.text, node.initializer);
|
|
2449
|
+
ts8.forEachChild(node, visitTop);
|
|
2122
2450
|
};
|
|
2123
2451
|
visitTop(source);
|
|
2124
2452
|
return specs;
|
|
2125
2453
|
}
|
|
2454
|
+
function isExportedWrapper(fn) {
|
|
2455
|
+
const declaration = ts8.isFunctionDeclaration(fn) ? fn : ts8.isVariableDeclaration(fn.parent) ? fn.parent.parent.parent : void 0;
|
|
2456
|
+
if (!declaration || !ts8.canHaveModifiers(declaration)) return false;
|
|
2457
|
+
return ts8.getModifiers(declaration)?.some((modifier) => modifier.kind === ts8.SyntaxKind.ExportKeyword) ?? false;
|
|
2458
|
+
}
|
|
2126
2459
|
function classifyOutboundCallsInSource(source, filePath) {
|
|
2127
2460
|
const calls = [];
|
|
2128
2461
|
const sourceFile = normalizePath(filePath);
|
|
@@ -2134,7 +2467,7 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
2134
2467
|
calls.push({ node, fact: { ...fact, sourceFile, sourceLine: lineOf4(source.text, node.getStart(source)), confidence: fact.confidence ?? 0.8, evidence: parserEvidence(source, node, extra) } });
|
|
2135
2468
|
};
|
|
2136
2469
|
const visit = (node) => {
|
|
2137
|
-
if (
|
|
2470
|
+
if (ts8.isCallExpression(node)) {
|
|
2138
2471
|
if (wrapperInternalRanges.some((range) => node.getStart(source) >= range.start && node.getEnd() <= range.end)) {
|
|
2139
2472
|
return;
|
|
2140
2473
|
}
|
|
@@ -2145,24 +2478,23 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
2145
2478
|
const entity = arg ? queryEntityFromAst(arg, initializers) : void 0;
|
|
2146
2479
|
const payload = arg?.getText(source) ?? "";
|
|
2147
2480
|
add(node, { callType: "local_db_query", queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? void 0 : queryWarning(payload) });
|
|
2148
|
-
} else if (
|
|
2481
|
+
} else if (ts8.isPropertyAccessExpression(expr) && expr.name.text === "send" && (ts8.isIdentifier(expr.expression) || ts8.isPropertyAccessExpression(expr.expression))) {
|
|
2149
2482
|
const objectArg = node.arguments[0];
|
|
2150
|
-
if (objectArg &&
|
|
2483
|
+
if (objectArg && ts8.isObjectLiteralExpression(objectArg)) {
|
|
2151
2484
|
const receiver = receiverName(expr.expression);
|
|
2152
2485
|
const query = objectPropertyText(objectArg, "query");
|
|
2153
2486
|
const method = stripQuotes(resolveExpression(propertyInitializer(objectArg, "method"), node, "literal").value ?? objectPropertyText(objectArg, "method") ?? "POST");
|
|
2154
2487
|
const pathExpr = propertyInitializer(objectArg, "path") ?? propertyInitializer(objectArg, "event");
|
|
2155
|
-
const
|
|
2156
|
-
const op = pathExpr ?
|
|
2488
|
+
const pathAnalysis = analyzeOperationPath(pathExpr, node, method);
|
|
2489
|
+
const op = pathExpr ? operationPathExpression(pathAnalysis) ?? pathExpr.getText(source) : void 0;
|
|
2157
2490
|
const shorthandPath = objectPropertyIsShorthand(objectArg, "path");
|
|
2158
|
-
const
|
|
2159
|
-
const candidateOperationPath = candidateEvidence2 && !candidateEvidence2.hasDynamicAssignments && candidateEvidence2.normalizedCandidateOperations.length === 1 && candidateEvidence2.candidatePaths.length > 0 ? candidateEvidence2.candidatePaths[0] : void 0;
|
|
2160
|
-
const operationPathExpr = operationPathFromStatic(resolvedPath.text) ?? candidateOperationPath;
|
|
2491
|
+
const operationPathExpr = operationPathExpression(pathAnalysis);
|
|
2161
2492
|
const intent = classifyODataPathIntent(operationPathExpr, method);
|
|
2162
2493
|
const entityCallTypes = { entity_mutation: "remote_entity_mutation", entity_delete: "remote_entity_delete", entity_media: "remote_entity_media", entity_candidate: "remote_entity_candidate" };
|
|
2163
2494
|
const entityCallType = entityCallTypes[intent.kind];
|
|
2164
2495
|
const isODataQueryRead = method.toUpperCase() === "GET" && ["entity_query", "entity_key_read", "entity_navigation_query"].includes(intent.kind);
|
|
2165
|
-
|
|
2496
|
+
const unresolvedReason = !query && pathExpr ? pathUnresolvedReason(pathAnalysis) : void 0;
|
|
2497
|
+
add(node, { callType: query ? "remote_query" : entityCallType ?? (isODataQueryRead ? "remote_query" : "remote_action"), serviceVariableName: receiver, method, operationPathExpr, queryEntity: query ? extractQueryEntity(query) : isODataQueryRead ? intent.entitySegment : void 0, payloadSummary: summarizeExpression(objectArg.getText(source)), confidence: op || query ? 0.8 : 0.4, unresolvedReason }, { receiver, classifier: "service_client_send_object", operationPathExpression: shorthandPath ? op : void 0, rawPathExpression: pathAnalysis.rawExpression, literalPathSource: literalPathSource(pathAnalysis), odataPathIntent: operationPathExpr ? intent : void 0, pathAnalysis, staticPathCandidates: legacyPathCandidates(pathAnalysis), parserWarning: unresolvedReason });
|
|
2166
2498
|
} else {
|
|
2167
2499
|
const receiver = receiverName(expr.expression);
|
|
2168
2500
|
const rootReceiver = rootReceiverName(expr.expression);
|
|
@@ -2171,33 +2503,35 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
2171
2503
|
const pathArg = node.arguments[1];
|
|
2172
2504
|
const supported = method && supportedHttpMethods.has(method);
|
|
2173
2505
|
if (receiver && supported && serviceVariables.has(rootReceiver ?? receiver)) {
|
|
2174
|
-
const
|
|
2175
|
-
const operationPathExpr =
|
|
2506
|
+
const pathAnalysis = analyzeOperationPath(pathArg, node, method);
|
|
2507
|
+
const operationPathExpr = operationPathExpression(pathAnalysis);
|
|
2176
2508
|
const intent = classifyODataPathIntent(operationPathExpr, method);
|
|
2177
|
-
|
|
2509
|
+
const unresolvedReason = pathUnresolvedReason(pathAnalysis);
|
|
2510
|
+
add(node, { callType: "remote_action", serviceVariableName: rootReceiver ?? receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: operationPathExpr ? 0.8 : 0.45, unresolvedReason }, { receiver, rootReceiver, classifier: "service_client_send_method_path", rawPathExpression: pathAnalysis.rawExpression, literalPathSource: literalPathSource(pathAnalysis), odataPathIntent: operationPathExpr ? intent : void 0, pathAnalysis, staticPathCandidates: legacyPathCandidates(pathAnalysis), parserWarning: unresolvedReason });
|
|
2178
2511
|
} else if (receiver && serviceVariables.has(rootReceiver ?? receiver)) {
|
|
2179
2512
|
const operationPathExpr = safeOperationName(firstArg2.value);
|
|
2180
2513
|
add(node, { callType: "remote_action", serviceVariableName: rootReceiver ?? receiver, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: operationPathExpr ? 0.75 : 0.35, unresolvedReason: operationPathExpr ? void 0 : "unsupported_cap_send_signature" }, { receiver, rootReceiver, classifier: operationPathExpr ? "service_client_send_operation_event" : "service_client_send_unsupported_signature", rawOperationExpression: firstArg2.rawExpression, literalOperationSource: firstArg2.value ? firstArg2.sourceKind : void 0, parserWarning: operationPathExpr ? void 0 : "unsupported_cap_send_signature" });
|
|
2181
2514
|
}
|
|
2182
2515
|
}
|
|
2183
|
-
} else if (
|
|
2184
|
-
const wrapperName =
|
|
2185
|
-
const wrapperArgs =
|
|
2516
|
+
} else if (ts8.isCallExpression(expr) && ts8.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text) || ts8.isIdentifier(expr) && wrapperSpecs.has(expr.text)) {
|
|
2517
|
+
const wrapperName = ts8.isIdentifier(expr) ? expr.text : ts8.isCallExpression(expr) && ts8.isIdentifier(expr.expression) ? expr.expression.text : "";
|
|
2518
|
+
const wrapperArgs = ts8.isIdentifier(expr) ? node.arguments : ts8.isCallExpression(expr) ? expr.arguments : node.arguments;
|
|
2186
2519
|
const spec = wrapperSpecs.get(wrapperName);
|
|
2187
2520
|
const clientArg = spec?.clientIndex === void 0 ? void 0 : wrapperArgs[spec.clientIndex];
|
|
2188
2521
|
const pathArg = spec ? wrapperArgs[spec.pathIndex] : void 0;
|
|
2189
2522
|
const methodArg = spec?.methodIndex === void 0 ? void 0 : wrapperArgs[spec.methodIndex];
|
|
2190
|
-
const receiver = clientArg &&
|
|
2523
|
+
const receiver = clientArg && ts8.isIdentifier(clientArg) ? clientArg.text : spec?.clientName;
|
|
2191
2524
|
const method = stripQuotes(resolveExpression(methodArg, node, "literal").value ?? spec?.methodLiteral ?? "POST");
|
|
2192
|
-
const
|
|
2193
|
-
const operationPathExpr =
|
|
2525
|
+
const pathAnalysis = analyzeOperationPath(pathArg, node, method);
|
|
2526
|
+
const operationPathExpr = operationPathExpression(pathAnalysis);
|
|
2194
2527
|
const normalizedOperationPath = operationPathExpr ? classifyODataPathIntent(operationPathExpr, method).topLevelOperationName : void 0;
|
|
2528
|
+
const unresolvedReason = pathUnresolvedReason(pathAnalysis);
|
|
2195
2529
|
if (spec && receiver && operationPathExpr) {
|
|
2196
|
-
add(node, { callType: "remote_action", serviceVariableName: receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.75 }, { receiver, classifier:
|
|
2530
|
+
add(node, { callType: "remote_action", serviceVariableName: receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.75, unresolvedReason }, { receiver, classifier: pathAnalysis.sourceKind.includes("string_literal") ? "higher_order_wrapper_literal_path" : "higher_order_wrapper_static_path", wrapperFunction: wrapperName, nestedWrapperFunction: spec.nestedWrapperFunction, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf4(source.text, node.getStart(source)), wrapperPathSourceKind: wrapperSourceKind(pathAnalysis.sourceKind), rawPathExpression: pathAnalysis.rawExpression, normalizedOperationPath, literalPathSource: pathAnalysis.sourceKind.includes("const_alias") ? "same_scope_const_initializer" : `wrapper_call_${wrapperSourceKind(pathAnalysis.sourceKind)}`, literalCallerArgumentDetected: true, pathAnalysis });
|
|
2197
2531
|
} else if (spec && receiver) {
|
|
2198
|
-
add(node, { callType: "remote_action", serviceVariableName: receiver, method, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.45, unresolvedReason
|
|
2532
|
+
add(node, { callType: "remote_action", serviceVariableName: receiver, method, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.45, unresolvedReason }, { receiver, classifier: pathAnalysis.status === "ambiguous" ? "higher_order_wrapper_ambiguous_path" : "higher_order_wrapper_dynamic_path", wrapperFunction: wrapperName, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf4(source.text, node.getStart(source)), wrapperPathSourceKind: wrapperSourceKind(pathAnalysis.sourceKind), rawPathExpression: pathAnalysis.rawExpression, pathAnalysis, parserWarning: unresolvedReason });
|
|
2199
2533
|
}
|
|
2200
|
-
} else if (
|
|
2534
|
+
} else if (ts8.isPropertyAccessExpression(expr) && ["emit", "publish", "on"].includes(expr.name.text)) {
|
|
2201
2535
|
const receiver = receiverName(expr.expression);
|
|
2202
2536
|
const rootReceiver = rootReceiverName(expr.expression);
|
|
2203
2537
|
if (isSupportedEventReceiver(receiver, rootReceiver, serviceVariables)) {
|
|
@@ -2213,7 +2547,7 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
2213
2547
|
}
|
|
2214
2548
|
}
|
|
2215
2549
|
}
|
|
2216
|
-
|
|
2550
|
+
ts8.forEachChild(node, visit);
|
|
2217
2551
|
};
|
|
2218
2552
|
visit(source);
|
|
2219
2553
|
return calls;
|
|
@@ -2226,21 +2560,21 @@ function containsSupportedOutboundCall(node) {
|
|
|
2226
2560
|
}
|
|
2227
2561
|
async function parseOutboundCalls(repoPath, filePath) {
|
|
2228
2562
|
const text = await fs7.readFile(path10.join(repoPath, filePath), "utf8");
|
|
2229
|
-
const source =
|
|
2563
|
+
const source = ts8.createSourceFile(filePath, text, ts8.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts8.ScriptKind.TS : ts8.ScriptKind.JS);
|
|
2230
2564
|
const bindingNames = new Set((await parseServiceBindings(repoPath, filePath)).map((binding) => binding.variableName));
|
|
2231
2565
|
const importedWrappers = await parseImportedWrapperCalls(repoPath, filePath, source, bindingNames);
|
|
2232
2566
|
return [...classifyOutboundCallsInSource(source, filePath).map((call) => call.fact), ...importedWrappers, ...parseLocalServiceCalls(text, filePath)];
|
|
2233
2567
|
}
|
|
2234
2568
|
function parseLocalServiceCalls(text, filePath) {
|
|
2235
|
-
const source =
|
|
2569
|
+
const source = ts8.createSourceFile(filePath, text, ts8.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts8.ScriptKind.TS : ts8.ScriptKind.JS);
|
|
2236
2570
|
const aliases = /* @__PURE__ */ new Map();
|
|
2237
2571
|
const calls = [];
|
|
2238
2572
|
const visit = (node) => {
|
|
2239
|
-
if (
|
|
2573
|
+
if (ts8.isVariableDeclaration(node) && ts8.isIdentifier(node.name) && node.initializer) {
|
|
2240
2574
|
const origin = serviceLookup(node.initializer, aliases);
|
|
2241
2575
|
if (origin) aliases.set(node.name.text, { ...origin, chain: [...origin.chain, node.name.text] });
|
|
2242
2576
|
}
|
|
2243
|
-
if (
|
|
2577
|
+
if (ts8.isCallExpression(node)) {
|
|
2244
2578
|
const parsed = serviceOperationCall(node, aliases);
|
|
2245
2579
|
if (parsed && parsed.operation !== "entities") calls.push({
|
|
2246
2580
|
callType: "local_service_call",
|
|
@@ -2263,20 +2597,20 @@ function parseLocalServiceCalls(text, filePath) {
|
|
|
2263
2597
|
})
|
|
2264
2598
|
});
|
|
2265
2599
|
}
|
|
2266
|
-
|
|
2600
|
+
ts8.forEachChild(node, visit);
|
|
2267
2601
|
};
|
|
2268
2602
|
visit(source);
|
|
2269
2603
|
return calls;
|
|
2270
2604
|
}
|
|
2271
2605
|
function serviceLookup(expr, aliases) {
|
|
2272
|
-
if (
|
|
2273
|
-
if (
|
|
2274
|
-
if (
|
|
2606
|
+
if (ts8.isIdentifier(expr)) return aliases.get(expr.text);
|
|
2607
|
+
if (ts8.isPropertyAccessExpression(expr) && expr.expression.getText() === "cds.services") return { service: expr.name.text, lookup: expr.getText(), chain: [expr.getText()] };
|
|
2608
|
+
if (ts8.isElementAccessExpression(expr) && expr.expression.getText() === "cds.services" && ts8.isStringLiteral(expr.argumentExpression)) return { service: expr.argumentExpression.text, lookup: expr.getText(), chain: [expr.getText()] };
|
|
2275
2609
|
return void 0;
|
|
2276
2610
|
}
|
|
2277
2611
|
function serviceOperationCall(node, aliases) {
|
|
2278
2612
|
const expr = node.expression;
|
|
2279
|
-
if (!
|
|
2613
|
+
if (!ts8.isPropertyAccessExpression(expr)) return void 0;
|
|
2280
2614
|
const origin = serviceLookup(expr.expression, aliases);
|
|
2281
2615
|
if (!origin) return void 0;
|
|
2282
2616
|
if (expr.name.text === "send") {
|
|
@@ -2710,46 +3044,6 @@ function linkHelperPackages(db, workspaceId, generation) {
|
|
|
2710
3044
|
return summary;
|
|
2711
3045
|
}
|
|
2712
3046
|
|
|
2713
|
-
// src/linker/operation-decorator-normalizer.ts
|
|
2714
|
-
function lowerFirst(value) {
|
|
2715
|
-
return value ? `${value[0]?.toLowerCase() ?? ""}${value.slice(1)}` : value;
|
|
2716
|
-
}
|
|
2717
|
-
function normalizedOperationName(value) {
|
|
2718
|
-
return value.replace(/^\//, "");
|
|
2719
|
-
}
|
|
2720
|
-
function clean(value) {
|
|
2721
|
-
return value.replace(/^[`'"]|[`'"]$/g, "");
|
|
2722
|
-
}
|
|
2723
|
-
function generatedFromConstantName(value) {
|
|
2724
|
-
for (const prefix of ["Action", "Func"]) {
|
|
2725
|
-
if (value.startsWith(prefix) && value.length > prefix.length && /^[A-Z]/.test(value.slice(prefix.length))) return lowerFirst(value.slice(prefix.length));
|
|
2726
|
-
}
|
|
2727
|
-
return void 0;
|
|
2728
|
-
}
|
|
2729
|
-
function resolved(value, raw) {
|
|
2730
|
-
const literal2 = clean(value);
|
|
2731
|
-
const generated = generatedFromConstantName(literal2);
|
|
2732
|
-
return { status: "resolved", operationName: generated ?? normalizedOperationName(literal2), raw };
|
|
2733
|
-
}
|
|
2734
|
-
function normalizeDecoratorOperationSignal(value, raw, candidateOperation) {
|
|
2735
|
-
if (value) return resolved(value, raw);
|
|
2736
|
-
if (!raw || raw.trim().length === 0) return { status: "none", raw };
|
|
2737
|
-
const expression = raw.trim();
|
|
2738
|
-
const nameMatch = /(?:^|\.)(Action[A-Z][\w$]*|Func[A-Z][\w$]*)\.name$/.exec(expression);
|
|
2739
|
-
if (nameMatch?.[1]) return resolved(nameMatch[1], expression);
|
|
2740
|
-
const stringMatch = /^String\(([A-Za-z_$][\w$]*)\)$/.exec(expression);
|
|
2741
|
-
if (stringMatch?.[1]) {
|
|
2742
|
-
const identifier = stringMatch[1];
|
|
2743
|
-
const generated = generatedFromConstantName(identifier);
|
|
2744
|
-
const normalizedCandidate = candidateOperation ? normalizedOperationName(candidateOperation) : void 0;
|
|
2745
|
-
if (generated) return { status: "resolved", operationName: generated, raw: expression };
|
|
2746
|
-
if (normalizedCandidate && identifier === normalizedCandidate) return { status: "resolved", operationName: identifier, raw: expression };
|
|
2747
|
-
return { status: "unsupported", raw: expression, reason: "string_wrapper_identifier_not_resolved" };
|
|
2748
|
-
}
|
|
2749
|
-
if (/^[`'"]/.test(expression) && !/[`'"]$/.test(expression)) return { status: "malformed", raw: expression, reason: "unterminated_literal" };
|
|
2750
|
-
return { status: "unsupported", raw: expression, reason: "unsupported_decorator_expression" };
|
|
2751
|
-
}
|
|
2752
|
-
|
|
2753
3047
|
// src/linker/cross-repo-linker.ts
|
|
2754
3048
|
function linkWorkspace(db, workspaceId, vars = {}) {
|
|
2755
3049
|
return db.transaction(() => {
|
|
@@ -2807,7 +3101,36 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
|
2807
3101
|
const operationLikeRemoteEntity = isRemoteEntityCall && Boolean(op) && credibleOperationSignal && (!strongEntitySignal || indexedOperationCandidateCount > 0);
|
|
2808
3102
|
const isOperationCall = operationLikeRemoteEntity || (callType === "remote_action" || callType === "local_service_call" || callType === "remote_query" && Boolean(op));
|
|
2809
3103
|
const resolution = isOperationCall ? resolveOperation(db, { servicePath, operationPath: op, serviceName: call.local_service_name, repoId: callType === "local_service_call" ? Number(call.repo_id) : void 0, alias: applyVariables(call.aliasExpr ?? call.alias, vars), destination: destination ? applyVariables(destination, vars) : void 0, isDynamic, hasExplicitOverride: Object.keys(vars).length > 0 || callType === "local_service_call" }, workspaceId) : { status: "unresolved", candidates: [], reasons: [] };
|
|
2810
|
-
const evidence = {
|
|
3104
|
+
const evidence = {
|
|
3105
|
+
...callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0, normalized, intent),
|
|
3106
|
+
indexedOperationCandidateCount,
|
|
3107
|
+
parserCallType: callType,
|
|
3108
|
+
entityOperationPrecedence: operationPrecedence(
|
|
3109
|
+
callType,
|
|
3110
|
+
intent,
|
|
3111
|
+
indexedOperationCandidateCount,
|
|
3112
|
+
Boolean(resolution.target)
|
|
3113
|
+
)
|
|
3114
|
+
};
|
|
3115
|
+
const pathAnalysis = objectValue(objectJson(call.evidence_json)?.pathAnalysis);
|
|
3116
|
+
if (callType === "remote_action" && pathAnalysis?.status === "ambiguous") {
|
|
3117
|
+
const candidatePaths = Array.isArray(pathAnalysis.candidateRawPaths) ? pathAnalysis.candidateRawPaths : [];
|
|
3118
|
+
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(
|
|
3119
|
+
workspaceId,
|
|
3120
|
+
"UNRESOLVED_EDGE",
|
|
3121
|
+
"ambiguous",
|
|
3122
|
+
"call",
|
|
3123
|
+
String(call.id),
|
|
3124
|
+
"operation_candidates",
|
|
3125
|
+
candidatePaths.join(","),
|
|
3126
|
+
Number(call.confidence ?? 0.5),
|
|
3127
|
+
JSON.stringify(evidence),
|
|
3128
|
+
0,
|
|
3129
|
+
"Ambiguous operation path candidates require explicit disambiguation",
|
|
3130
|
+
generation
|
|
3131
|
+
);
|
|
3132
|
+
return { status: "ambiguous", callType };
|
|
3133
|
+
}
|
|
2811
3134
|
if (isRemoteEntityCall && (resolution.target || resolution.candidates.length > 0 || resolution.status === "dynamic")) {
|
|
2812
3135
|
if (resolution.target) {
|
|
2813
3136
|
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "REMOTE_CALL_RESOLVES_TO_OPERATION", "resolved", "call", String(call.id), "operation", String(resolution.target.operationId), resolution.target.score, JSON.stringify({ ...evidence, operationEntityPrecedence: "indexed_operation_over_parser_entity" }), 0, generation);
|
|
@@ -2853,6 +3176,34 @@ function operationCandidateCount(db, workspaceId, operationPath, operationName)
|
|
|
2853
3176
|
const row = db.prepare(`SELECT COUNT(*) count FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (o.operation_path=? OR o.operation_path=? OR o.operation_name=?)`).get(workspaceId, operationPath, normalizedName ? `/${normalizedName}` : operationPath, normalizedName);
|
|
2854
3177
|
return Number(row?.count ?? 0);
|
|
2855
3178
|
}
|
|
3179
|
+
function operationPrecedence(callType, intent, indexedOperationCandidateCount, resolvedOperation) {
|
|
3180
|
+
if (resolvedOperation) {
|
|
3181
|
+
return {
|
|
3182
|
+
decision: "operation",
|
|
3183
|
+
reason: "indexed_operation_with_strong_service_context",
|
|
3184
|
+
indexedOperationCandidateCount
|
|
3185
|
+
};
|
|
3186
|
+
}
|
|
3187
|
+
if (callType === "remote_action" && intent.kind === "operation_invocation") {
|
|
3188
|
+
return {
|
|
3189
|
+
decision: "operation_candidate",
|
|
3190
|
+
rejectionReason: indexedOperationCandidateCount > 0 ? "indexed_candidates_lack_unique_strong_service_context" : "no_indexed_operation_candidate",
|
|
3191
|
+
indexedOperationCandidateCount
|
|
3192
|
+
};
|
|
3193
|
+
}
|
|
3194
|
+
if (intent.kind.startsWith("entity_")) {
|
|
3195
|
+
return {
|
|
3196
|
+
decision: "entity",
|
|
3197
|
+
rejectionReason: indexedOperationCandidateCount > 0 ? "entity_shape_has_precedence_without_resolved_operation_context" : "entity_shape_has_no_indexed_operation_evidence",
|
|
3198
|
+
indexedOperationCandidateCount
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
3201
|
+
return {
|
|
3202
|
+
decision: "unresolved",
|
|
3203
|
+
rejectionReason: "path_has_no_safe_entity_or_operation_precedence",
|
|
3204
|
+
indexedOperationCandidateCount
|
|
3205
|
+
};
|
|
3206
|
+
}
|
|
2856
3207
|
function unresolvedOperationReason(resolution) {
|
|
2857
3208
|
if (resolution.status === "dynamic") return `Dynamic target requires runtime variable overrides: ${(resolution.reasons.length ? resolution.reasons : ["missing runtime variables"]).join(", ")}`;
|
|
2858
3209
|
if (resolution.candidates.length === 0) return "No indexed target operation matched";
|
|
@@ -2875,7 +3226,7 @@ function objectJson(value) {
|
|
|
2875
3226
|
}
|
|
2876
3227
|
function callEvidence(call, resolution, servicePath, op, destination, normalized, odataPathIntent) {
|
|
2877
3228
|
const bindingHasDynamicExpression = Boolean(Number(call.isDynamic ?? 0));
|
|
2878
|
-
const routingPlaceholderKeys = placeholderKeys([servicePath, destination,
|
|
3229
|
+
const routingPlaceholderKeys = placeholderKeys([servicePath, destination, stringValue3(call.aliasExpr), stringValue3(call.alias)]);
|
|
2879
3230
|
return { sourceFile: call.source_file, sourceLine: call.source_line, file: call.source_file, line: call.source_line, callId: call.id, repo: call.repoName, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination, servicePath, operationPath: op, rawOperationPath: normalized?.wasInvocation ? normalized.rawOperationPath : odataPathIntent?.rawPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : void 0, invocationArguments: normalized?.wasInvocation ? normalized.invocationArguments : void 0, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : void 0, routingPlaceholderKeys: routingPlaceholderKeys.length ? routingPlaceholderKeys : void 0, odataOperationNormalizationReason: normalized?.normalizationReason, odataOperationNormalizationRejectedReason: normalized?.normalizationRejectedReason, localServiceName: call.local_service_name, localServiceLookup: call.local_service_lookup, aliasChain: parseJson(call.alias_chain_json), transport: call.call_type === "local_service_call" ? "local" : void 0, targetRepo: resolution.target?.repoName, targetServicePath: resolution.target?.servicePath, targetOperationPath: resolution.target?.operationPath, targetOperation: resolution.target?.operationName, helperChain: parseJson(call.helperChainJson), candidates: resolution.candidates, candidateScores: compactCandidateScores(resolution.candidates), candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, odataPathIntent, queryStringPresent: odataPathIntent?.hasQueryString || void 0, queryPlaceholderKeys: odataPathIntent?.placeholderKeys.length ? odataPathIntent.placeholderKeys : void 0, bindingHasDynamicExpression: bindingHasDynamicExpression || void 0, outboundEvidence: objectJson(call.evidence_json), analysisCompleteness: call.unresolved_reason ? "partial" : "complete", parserWarning: call.unresolved_reason ? { code: "parser_warning", message: call.unresolved_reason } : void 0 };
|
|
2880
3231
|
}
|
|
2881
3232
|
function compactCandidateScores(candidates) {
|
|
@@ -2892,7 +3243,7 @@ function placeholderKeys(values) {
|
|
|
2892
3243
|
function objectValue(value) {
|
|
2893
3244
|
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2894
3245
|
}
|
|
2895
|
-
function
|
|
3246
|
+
function stringValue3(value) {
|
|
2896
3247
|
return typeof value === "string" ? value : void 0;
|
|
2897
3248
|
}
|
|
2898
3249
|
function linkImplementations(db, workspaceId, generation) {
|
|
@@ -2911,7 +3262,7 @@ function linkImplementations(db, workspaceId, generation) {
|
|
|
2911
3262
|
const duplicateFamilies = duplicatePackageFamilies(accepted);
|
|
2912
3263
|
const duplicatePackageAmbiguous = duplicateFamilies.length > 0 && !accepted.some(hasDirectOwnershipEvidence);
|
|
2913
3264
|
const selected = duplicatePackageAmbiguous ? accepted : winners;
|
|
2914
|
-
const
|
|
3265
|
+
const unique2 = !duplicatePackageAmbiguous && winners.length === 1 ? winners[0] : void 0;
|
|
2915
3266
|
const ambiguityReasons = duplicatePackageAmbiguous ? ["duplicate_package_name_candidates"] : winners.length > 1 ? ["multiple_equal_score_implementation_candidates"] : [];
|
|
2916
3267
|
const evidence = {
|
|
2917
3268
|
servicePath: operation.servicePath,
|
|
@@ -2925,16 +3276,16 @@ function linkImplementations(db, workspaceId, generation) {
|
|
|
2925
3276
|
candidateFamilies: duplicateFamilies,
|
|
2926
3277
|
candidates: candidates.map((candidate, index) => candidateEvidence(candidate, index + 1))
|
|
2927
3278
|
};
|
|
2928
|
-
const evidenceWithHints =
|
|
3279
|
+
const evidenceWithHints = unique2 ? evidence : { ...evidence, implementationHintSuggestions: implementationHintSuggestions(evidence) };
|
|
2929
3280
|
if (accepted.length === 0) {
|
|
2930
3281
|
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "OPERATION_IMPLEMENTED_BY_HANDLER", "unresolved", "operation", graphId(operation.operationId), "handler_method_candidates", candidates.map((row) => graphId(row.methodId)).join(","), 0, JSON.stringify(evidenceWithHints), 0, "No implementation candidate passed policy", generation);
|
|
2931
3282
|
edgeCount += 1;
|
|
2932
3283
|
unresolvedCount += 1;
|
|
2933
3284
|
continue;
|
|
2934
3285
|
}
|
|
2935
|
-
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "OPERATION_IMPLEMENTED_BY_HANDLER",
|
|
3286
|
+
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "OPERATION_IMPLEMENTED_BY_HANDLER", unique2 ? "resolved" : "ambiguous", "operation", graphId(operation.operationId), unique2 ? "handler_method" : "handler_method_candidates", unique2 ? graphId(unique2.methodId) : selected.map((row) => graphId(row.methodId)).join(","), unique2 ? 0.95 : 0.5, JSON.stringify(evidenceWithHints), 0, unique2 ? null : "Ambiguous registered handler implementation candidates", generation);
|
|
2936
3287
|
edgeCount += 1;
|
|
2937
|
-
if (
|
|
3288
|
+
if (unique2) resolvedCount += 1;
|
|
2938
3289
|
else ambiguousCount += 1;
|
|
2939
3290
|
}
|
|
2940
3291
|
return { edgeCount, resolvedCount, ambiguousCount, unresolvedCount };
|
|
@@ -2947,13 +3298,54 @@ function implementationContextForOperation(db, operation) {
|
|
|
2947
3298
|
}
|
|
2948
3299
|
function rankedImplementationCandidates(db, workspaceId, operation) {
|
|
2949
3300
|
const rows2 = implementationCandidates(db, workspaceId, operation);
|
|
2950
|
-
|
|
3301
|
+
const invalid = rows2.filter((row) => !validRegistrationPair(row));
|
|
3302
|
+
recordRegistrationInvariantDiagnostics(db, invalid);
|
|
3303
|
+
return deduplicateCandidates(
|
|
3304
|
+
rows2.filter(validRegistrationPair).map((row) => scoreImplementationCandidate(row, operation))
|
|
3305
|
+
).sort((a, b) => b.score - a.score || String(a.className).localeCompare(String(b.className)) || a.methodId - b.methodId);
|
|
3306
|
+
}
|
|
3307
|
+
function validRegistrationPair(row) {
|
|
3308
|
+
if (row.registrationHandlerClassId === null || row.registrationHandlerClassId === void 0)
|
|
3309
|
+
return registrationPairingStrategy(row) !== "unproven";
|
|
3310
|
+
return Number(row.registrationHandlerClassId) === Number(row.classId);
|
|
3311
|
+
}
|
|
3312
|
+
function registrationPairingStrategy(row) {
|
|
3313
|
+
if (row.registrationHandlerClassId !== null && row.registrationHandlerClassId !== void 0)
|
|
3314
|
+
return "exact_handler_class_id";
|
|
3315
|
+
if (Number(row.applicationRepoId) === Number(row.handlerRepoId))
|
|
3316
|
+
return "same_repository_class_name_fallback";
|
|
3317
|
+
const source = stringValue3(row.importSource);
|
|
3318
|
+
const separator = source?.lastIndexOf("#") ?? -1;
|
|
3319
|
+
if (!source || separator <= 0) return "unproven";
|
|
3320
|
+
const moduleName = source.slice(0, separator);
|
|
3321
|
+
const importedName = source.slice(separator + 1);
|
|
3322
|
+
const matchesClass = importedName === row.className || importedName === "default" && row.registrationClassName === row.className;
|
|
3323
|
+
return moduleName === row.handlerPackage && matchesClass ? "explicit_package_import" : "unproven";
|
|
3324
|
+
}
|
|
3325
|
+
function recordRegistrationInvariantDiagnostics(db, rows2) {
|
|
3326
|
+
const insert = db.prepare(`INSERT INTO diagnostics(repo_id,severity,code,message,source_file,source_line)
|
|
3327
|
+
SELECT ?,'error','handler_registration_class_mismatch',
|
|
3328
|
+
'Implementation candidate registration did not match its persisted handler class id',?,?
|
|
3329
|
+
WHERE NOT EXISTS (
|
|
3330
|
+
SELECT 1 FROM diagnostics
|
|
3331
|
+
WHERE repo_id=? AND code='handler_registration_class_mismatch'
|
|
3332
|
+
AND source_file=? AND source_line=?
|
|
3333
|
+
)`);
|
|
3334
|
+
for (const row of rows2)
|
|
3335
|
+
insert.run(
|
|
3336
|
+
row.applicationRepoId,
|
|
3337
|
+
row.registrationFile,
|
|
3338
|
+
row.registrationLine,
|
|
3339
|
+
row.applicationRepoId,
|
|
3340
|
+
row.registrationFile,
|
|
3341
|
+
row.registrationLine
|
|
3342
|
+
);
|
|
2951
3343
|
}
|
|
2952
3344
|
function deduplicateCandidates(rows2) {
|
|
2953
3345
|
const merged = /* @__PURE__ */ new Map();
|
|
2954
3346
|
for (const row of rows2) {
|
|
2955
3347
|
const key = [row.methodId, row.classId, row.handlerRepoId].join(":");
|
|
2956
|
-
const registration =
|
|
3348
|
+
const registration = registrationEvidence(row);
|
|
2957
3349
|
const existing = merged.get(key);
|
|
2958
3350
|
if (!existing) {
|
|
2959
3351
|
merged.set(key, { ...row, registrations: [registration] });
|
|
@@ -2967,6 +3359,17 @@ function deduplicateCandidates(rows2) {
|
|
|
2967
3359
|
}
|
|
2968
3360
|
return [...merged.values()];
|
|
2969
3361
|
}
|
|
3362
|
+
function registrationEvidence(row) {
|
|
3363
|
+
return {
|
|
3364
|
+
id: row.registrationId,
|
|
3365
|
+
handlerClassId: row.registrationHandlerClassId,
|
|
3366
|
+
file: row.registrationFile,
|
|
3367
|
+
line: row.registrationLine,
|
|
3368
|
+
kind: row.registrationKind,
|
|
3369
|
+
importSource: row.importSource,
|
|
3370
|
+
pairingStrategy: registrationPairingStrategy(row)
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
2970
3373
|
function uniqueRegistrations(rows2) {
|
|
2971
3374
|
const seen = /* @__PURE__ */ new Set();
|
|
2972
3375
|
return rows2.filter((row) => {
|
|
@@ -2995,10 +3398,14 @@ function implementationCandidates(db, workspaceId, operation) {
|
|
|
2995
3398
|
hm.method_name methodName,
|
|
2996
3399
|
hm.decorator_value decoratorValue,
|
|
2997
3400
|
hm.decorator_raw_expression decoratorRawExpression,
|
|
3401
|
+
hm.decorator_resolution_json decoratorResolutionJson,
|
|
2998
3402
|
hc.id classId,
|
|
2999
3403
|
hc.class_name className,
|
|
3000
3404
|
hc.source_file sourceFile,
|
|
3001
3405
|
hc.source_line sourceLine,
|
|
3406
|
+
hr.id registrationId,
|
|
3407
|
+
hr.handler_class_id registrationHandlerClassId,
|
|
3408
|
+
hr.class_name registrationClassName,
|
|
3002
3409
|
hr.repo_id applicationRepoId,
|
|
3003
3410
|
hr.registration_file registrationFile,
|
|
3004
3411
|
hr.registration_line registrationLine,
|
|
@@ -3021,14 +3428,36 @@ function implementationCandidates(db, workspaceId, operation) {
|
|
|
3021
3428
|
CASE WHEN appRepo.id=handlerRepo.id THEN 1 ELSE 0 END sameRepoRegistration,
|
|
3022
3429
|
CASE WHEN EXISTS (SELECT 1 FROM cds_services localService WHERE localService.repo_id=appRepo.id AND localService.service_path=?) THEN 1 ELSE 0 END localServicePathMatch,
|
|
3023
3430
|
CASE WHEN EXISTS (SELECT 1 FROM cds_services localService WHERE localService.repo_id=appRepo.id) THEN 1 ELSE 0 END applicationHasLocalServices,
|
|
3024
|
-
CASE WHEN EXISTS (SELECT 1 FROM handler_registrations localReg JOIN handler_classes localClass ON (localClass.id=localReg.handler_class_id OR localClass.class_name=localReg.class_name) JOIN handler_methods localMethod ON localMethod.handler_class_id=localClass.id WHERE localReg.repo_id=appRepo.id AND (localMethod.decorator_value=? OR localMethod.decorator_value=? OR localMethod.method_name=? OR localMethod.decorator_raw_expression LIKE ?)) THEN 1 ELSE 0 END applicationHasLocalRegistrationForOperation,
|
|
3431
|
+
CASE WHEN EXISTS (SELECT 1 FROM handler_registrations localReg JOIN handler_classes localClass ON ((localReg.handler_class_id IS NOT NULL AND localClass.id=localReg.handler_class_id) OR (localReg.handler_class_id IS NULL AND localClass.class_name=localReg.class_name AND localClass.repo_id=localReg.repo_id)) JOIN handler_methods localMethod ON localMethod.handler_class_id=localClass.id WHERE localReg.repo_id=appRepo.id AND (localMethod.decorator_value=? OR localMethod.decorator_value=? OR localMethod.method_name=? OR localMethod.decorator_raw_expression LIKE ?)) THEN 1 ELSE 0 END applicationHasLocalRegistrationForOperation,
|
|
3025
3432
|
CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(appRepo.id AS TEXT) AND dep.to_id=?) THEN 1 ELSE 0 END appDependsOnModel,
|
|
3026
3433
|
CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(appRepo.id AS TEXT) AND dep.to_id=CAST(handlerRepo.id AS TEXT)) THEN 1 ELSE 0 END appDependsOnHandler,
|
|
3027
3434
|
CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(handlerRepo.id AS TEXT) AND dep.to_id=?) THEN 1 ELSE 0 END handlerDependsOnModel
|
|
3028
3435
|
FROM handler_methods hm
|
|
3029
3436
|
JOIN handler_classes hc ON hc.id=hm.handler_class_id
|
|
3030
3437
|
JOIN repositories handlerRepo ON handlerRepo.id=hc.repo_id
|
|
3031
|
-
JOIN handler_registrations hr ON (
|
|
3438
|
+
JOIN handler_registrations hr ON (
|
|
3439
|
+
(hr.handler_class_id IS NOT NULL AND hr.handler_class_id=hc.id)
|
|
3440
|
+
OR (
|
|
3441
|
+
hr.handler_class_id IS NULL
|
|
3442
|
+
AND (
|
|
3443
|
+
(hr.class_name=hc.class_name AND hr.repo_id=hc.repo_id)
|
|
3444
|
+
OR (
|
|
3445
|
+
instr(hr.import_source,'#')>1
|
|
3446
|
+
AND substr(hr.import_source,1,instr(hr.import_source,'#')-1)
|
|
3447
|
+
=handlerRepo.package_name
|
|
3448
|
+
AND (
|
|
3449
|
+
substr(hr.import_source,instr(hr.import_source,'#')+1)
|
|
3450
|
+
=hc.class_name
|
|
3451
|
+
OR (
|
|
3452
|
+
substr(hr.import_source,instr(hr.import_source,'#')+1)
|
|
3453
|
+
='default'
|
|
3454
|
+
AND hr.class_name=hc.class_name
|
|
3455
|
+
)
|
|
3456
|
+
)
|
|
3457
|
+
)
|
|
3458
|
+
)
|
|
3459
|
+
)
|
|
3460
|
+
)
|
|
3032
3461
|
JOIN repositories appRepo ON appRepo.id=hr.repo_id
|
|
3033
3462
|
WHERE appRepo.workspace_id=?
|
|
3034
3463
|
AND (hm.decorator_value=? OR hm.decorator_value=? OR hm.method_name=? OR hm.decorator_raw_expression LIKE ?)`).all(
|
|
@@ -3130,8 +3559,16 @@ function candidateEvidence(candidate, rank) {
|
|
|
3130
3559
|
className: candidate.className,
|
|
3131
3560
|
sourceFile: candidate.sourceFile,
|
|
3132
3561
|
sourceLine: candidate.sourceLine,
|
|
3133
|
-
|
|
3562
|
+
decoratorResolution: objectJson(candidate.decoratorResolutionJson),
|
|
3563
|
+
registration: registrationEvidence(candidate),
|
|
3134
3564
|
registrations: candidate.registrations ?? [],
|
|
3565
|
+
registrationPairing: {
|
|
3566
|
+
strategy: registrationPairingStrategy(candidate),
|
|
3567
|
+
registrationId: candidate.registrationId,
|
|
3568
|
+
registrationHandlerClassId: candidate.registrationHandlerClassId,
|
|
3569
|
+
candidateHandlerClassId: candidate.classId,
|
|
3570
|
+
invariantStatus: validRegistrationPair(candidate) ? "valid" : "invalid"
|
|
3571
|
+
},
|
|
3135
3572
|
applicationPackage: { id: candidate.applicationRepoId, name: candidate.applicationRepo, packageName: candidate.applicationPackage },
|
|
3136
3573
|
handlerPackage: { id: candidate.handlerRepoId, name: candidate.handlerRepo, packageName: candidate.handlerPackage },
|
|
3137
3574
|
modelPackage: { id: candidate.modelRepoId, name: candidate.modelRepo, packageName: candidate.modelPackage },
|
|
@@ -3171,6 +3608,40 @@ function normalizedOperation(value) {
|
|
|
3171
3608
|
return value.startsWith("/") ? value.slice(1) : value;
|
|
3172
3609
|
}
|
|
3173
3610
|
|
|
3611
|
+
// src/trace/selectors.ts
|
|
3612
|
+
function parseVars(values) {
|
|
3613
|
+
const out = {};
|
|
3614
|
+
for (const value of values ?? []) {
|
|
3615
|
+
const [key, ...rest] = value.split("=");
|
|
3616
|
+
if (key && rest.length > 0) out[key] = rest.join("=");
|
|
3617
|
+
}
|
|
3618
|
+
return out;
|
|
3619
|
+
}
|
|
3620
|
+
function ambiguousStartDiagnostic(requested, candidates, message) {
|
|
3621
|
+
const serviceSuggestions = [...new Set(candidates.flatMap((row) => typeof row.servicePath === "string" ? [`--service ${row.servicePath}`] : []))].sort();
|
|
3622
|
+
return {
|
|
3623
|
+
severity: "warning",
|
|
3624
|
+
code: "trace_start_ambiguous",
|
|
3625
|
+
message,
|
|
3626
|
+
normalizedSelectorValue: requested,
|
|
3627
|
+
resolutionStage: "operation",
|
|
3628
|
+
resolutionStatus: "ambiguous_operation",
|
|
3629
|
+
candidates,
|
|
3630
|
+
serviceSuggestions,
|
|
3631
|
+
selectorSuggestions: fullSelectorSuggestions(candidates)
|
|
3632
|
+
};
|
|
3633
|
+
}
|
|
3634
|
+
function fullSelectorSuggestions(candidates) {
|
|
3635
|
+
const includeRepo = new Set(candidates.map((row) => row.repoName)).size > 1;
|
|
3636
|
+
return [...new Set(candidates.flatMap((row) => {
|
|
3637
|
+
if (typeof row.servicePath !== "string" || typeof row.operationPath !== "string") return [];
|
|
3638
|
+
const repoSelector = includeRepo && typeof row.repoName === "string" ? `--repo ${row.repoName} ` : "";
|
|
3639
|
+
return [
|
|
3640
|
+
`${repoSelector}--service ${row.servicePath} --path ${row.operationPath}`
|
|
3641
|
+
];
|
|
3642
|
+
}))].sort();
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3174
3645
|
// src/trace/evidence.ts
|
|
3175
3646
|
function baseTraceEvidence(row, call, persistedEvidence, contextualEvidence) {
|
|
3176
3647
|
const evidence = { ...persistedEvidence, ...contextualEvidence ?? {} };
|
|
@@ -3189,11 +3660,12 @@ function baseTraceEvidence(row, call, persistedEvidence, contextualEvidence) {
|
|
|
3189
3660
|
persistedResolution: persistedResolution(row)
|
|
3190
3661
|
};
|
|
3191
3662
|
}
|
|
3192
|
-
function runtimeResolution(db, row, evidence, vars, workspaceId) {
|
|
3663
|
+
function runtimeResolution(db, row, evidence, vars, workspaceId, contextualUnresolvedReason) {
|
|
3193
3664
|
const substituted = evidenceWithRuntimeVariables(evidence, vars);
|
|
3194
3665
|
if (!isRemoteRuntimeCandidate(row, evidence, vars)) {
|
|
3195
|
-
const
|
|
3196
|
-
|
|
3666
|
+
const unresolvedReason2 = contextualUnresolvedReason ?? row.unresolved_reason;
|
|
3667
|
+
const withSections = withEffectiveResolution(substituted, row, unresolvedReason2);
|
|
3668
|
+
return { row, evidence: withSections, unresolvedReason: unresolvedReason2 };
|
|
3197
3669
|
}
|
|
3198
3670
|
const resolution = resolveRuntimeOperation(db, substituted, workspaceId);
|
|
3199
3671
|
if (resolution.target) {
|
|
@@ -3206,6 +3678,9 @@ function runtimeResolution(db, row, evidence, vars, workspaceId) {
|
|
|
3206
3678
|
function runtimeVariableDiagnostic(edges) {
|
|
3207
3679
|
const missing = /* @__PURE__ */ new Set();
|
|
3208
3680
|
for (const edge of edges) {
|
|
3681
|
+
const effective = parseObject(edge.evidence.effectiveResolution);
|
|
3682
|
+
if (!["dynamic", "unresolved", "ambiguous"].includes(String(effective.status ?? "")))
|
|
3683
|
+
continue;
|
|
3209
3684
|
const substitutions = edge.evidence.runtimeSubstitutions;
|
|
3210
3685
|
if (!substitutions || typeof substitutions !== "object" || Array.isArray(substitutions)) continue;
|
|
3211
3686
|
for (const value of Object.values(substitutions))
|
|
@@ -3223,20 +3698,20 @@ function runtimeVariableDiagnostic(edges) {
|
|
|
3223
3698
|
}
|
|
3224
3699
|
function edgeTarget(row, evidence) {
|
|
3225
3700
|
const effective = parseObject(evidence.effectiveResolution);
|
|
3226
|
-
const targetServicePath =
|
|
3227
|
-
const targetOperationPath =
|
|
3701
|
+
const targetServicePath = stringValue4(effective.targetServicePath ?? evidence.targetServicePath);
|
|
3702
|
+
const targetOperationPath = stringValue4(effective.targetOperationPath ?? evidence.targetOperationPath);
|
|
3228
3703
|
if (targetServicePath && targetOperationPath) return `${targetServicePath}${targetOperationPath}`;
|
|
3229
3704
|
const runtimeCandidate = evidence.runtimeResolvedCandidate;
|
|
3230
3705
|
if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath) return `${runtimeCandidate.servicePath}${runtimeCandidate.operationPath}`;
|
|
3231
|
-
const servicePath =
|
|
3232
|
-
const operationPath =
|
|
3233
|
-
const targetOperation =
|
|
3234
|
-
const targetRepo =
|
|
3706
|
+
const servicePath = stringValue4(evidence.servicePath);
|
|
3707
|
+
const operationPath = stringValue4(evidence.operationPath);
|
|
3708
|
+
const targetOperation = stringValue4(evidence.targetOperation);
|
|
3709
|
+
const targetRepo = stringValue4(evidence.targetRepo) ?? "";
|
|
3235
3710
|
if (row.edge_type === "HANDLER_RUNS_DB_QUERY") return `Entity: ${row.to_id || "unknown"}`;
|
|
3236
|
-
if (row.edge_type === "HANDLER_RUNS_REMOTE_QUERY") return
|
|
3711
|
+
if (row.edge_type === "HANDLER_RUNS_REMOTE_QUERY") return stringValue4(evidence.remoteQueryTarget) ?? `Remote query: ${row.to_id || "unknown"}`;
|
|
3237
3712
|
if (row.edge_type === "HANDLER_CALLS_EXTERNAL_HTTP") {
|
|
3238
3713
|
const target = parseObject(evidence.externalTarget);
|
|
3239
|
-
return
|
|
3714
|
+
return stringValue4(target.label) ?? `External endpoint: ${row.to_id || "unknown"}`;
|
|
3240
3715
|
}
|
|
3241
3716
|
if (servicePath && operationPath) return `${servicePath}${operationPath}`;
|
|
3242
3717
|
return targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
|
|
@@ -3275,12 +3750,12 @@ function withEffectiveResolution(evidence, row, unresolvedReason, resolution) {
|
|
|
3275
3750
|
return { ...rest, effectiveResolution: current, linker: { status: current.status, confidence: current.confidence, reason: unresolvedReason, edgeType: current.edgeType } };
|
|
3276
3751
|
}
|
|
3277
3752
|
function resolveRuntimeOperation(db, evidence, workspaceId) {
|
|
3278
|
-
const servicePath =
|
|
3279
|
-
const rawOperationPath =
|
|
3753
|
+
const servicePath = stringValue4(evidence.servicePath);
|
|
3754
|
+
const rawOperationPath = stringValue4(evidence.operationPath);
|
|
3280
3755
|
const normalized = normalizeODataOperationInvocationPath(rawOperationPath);
|
|
3281
|
-
const operationPath = normalized?.wasInvocation ? normalized.normalizedOperationPath :
|
|
3282
|
-
const alias =
|
|
3283
|
-
const destination =
|
|
3756
|
+
const operationPath = normalized?.wasInvocation ? normalized.normalizedOperationPath : stringValue4(evidence.normalizedOperationPath) ?? rawOperationPath;
|
|
3757
|
+
const alias = stringValue4(evidence.serviceAliasExpr ?? evidence.serviceAlias);
|
|
3758
|
+
const destination = stringValue4(evidence.destination);
|
|
3284
3759
|
return resolveOperation(db, { servicePath, operationPath, alias, destination, hasExplicitOverride: true, isDynamic: true }, workspaceId);
|
|
3285
3760
|
}
|
|
3286
3761
|
function evidenceWithRuntimeVariables(evidence, vars) {
|
|
@@ -3295,11 +3770,17 @@ function evidenceWithRuntimeVariables(evidence, vars) {
|
|
|
3295
3770
|
function runtimeSubstitutions(evidence, vars) {
|
|
3296
3771
|
const substitutions = {};
|
|
3297
3772
|
for (const key of ["servicePath", "operationPath", "serviceAliasExpr", "serviceAlias", "destination"]) {
|
|
3298
|
-
const substitution = substituteVariables(
|
|
3773
|
+
const substitution = substituteVariables(substitutionValue(evidence, key), vars);
|
|
3299
3774
|
if (substitution.placeholders.length > 0) substitutions[key] = substitution;
|
|
3300
3775
|
}
|
|
3301
3776
|
return substitutions;
|
|
3302
3777
|
}
|
|
3778
|
+
function substitutionValue(evidence, key) {
|
|
3779
|
+
const value = stringValue4(evidence[key]);
|
|
3780
|
+
if (key !== "operationPath") return value;
|
|
3781
|
+
const normalized = normalizeODataOperationInvocationPath(value);
|
|
3782
|
+
return normalized?.wasInvocation ? normalized.normalizedOperationPath : value;
|
|
3783
|
+
}
|
|
3303
3784
|
function isRemoteRuntimeCandidate(row, evidence, vars) {
|
|
3304
3785
|
if (!vars || Object.keys(vars).length === 0) return false;
|
|
3305
3786
|
if (!["dynamic", "ambiguous", "unresolved"].includes(String(row.status ?? ""))) return false;
|
|
@@ -3317,7 +3798,7 @@ function runtimeUnresolvedReason(resolution) {
|
|
|
3317
3798
|
function parseObject(value) {
|
|
3318
3799
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3319
3800
|
}
|
|
3320
|
-
function
|
|
3801
|
+
function stringValue4(value) {
|
|
3321
3802
|
return typeof value === "string" ? value : void 0;
|
|
3322
3803
|
}
|
|
3323
3804
|
|
|
@@ -3332,32 +3813,40 @@ function positiveDepth(value) {
|
|
|
3332
3813
|
function operationStartScope(db, repoId, start, hintOptions) {
|
|
3333
3814
|
const requested = normalizeOperation(start.operationPath ?? start.operation);
|
|
3334
3815
|
if (!requested) return void 0;
|
|
3335
|
-
const rows2 = db.prepare(`SELECT o.id operationId,o.operation_name operationName,o.operation_path operationPath,s.service_path servicePath,r.id repoId,r.name repoName
|
|
3816
|
+
const rows2 = db.prepare(`SELECT o.id operationId,o.operation_name operationName,o.operation_path operationPath,o.source_file sourceFile,o.source_line sourceLine,s.service_path servicePath,r.id repoId,r.name repoName
|
|
3336
3817
|
FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
|
|
3337
3818
|
WHERE (? IS NULL OR r.id=?) AND (? IS NULL OR s.service_path=?) AND (o.operation_name=? OR o.operation_path=? OR o.operation_path=?)
|
|
3338
3819
|
ORDER BY r.name,s.service_path,o.operation_name,o.id`).all(repoId, repoId, start.servicePath, start.servicePath, requested, requested, requested.startsWith("/") ? requested : `/${requested}`);
|
|
3339
3820
|
if (rows2.length === 0) return void 0;
|
|
3340
3821
|
const repoCount = new Set(rows2.map((row) => String(row.repoName))).size;
|
|
3341
3822
|
const serviceCount = new Set(rows2.map((row) => `${String(row.repoName)}:${String(row.servicePath)}`)).size;
|
|
3342
|
-
if (!repoId && repoCount > 1)
|
|
3343
|
-
|
|
3344
|
-
if (
|
|
3823
|
+
if (!repoId && repoCount > 1)
|
|
3824
|
+
return { diagnostics: [ambiguousStartDiagnostic(requested, rows2, "Operation trace start matched multiple repositories; add --repo to disambiguate")] };
|
|
3825
|
+
if (!start.servicePath && serviceCount > 1)
|
|
3826
|
+
return { diagnostics: [ambiguousStartDiagnostic(requested, rows2, "Operation trace start matched multiple services; add --service to disambiguate")] };
|
|
3827
|
+
if (rows2.length !== 1)
|
|
3828
|
+
return { diagnostics: [ambiguousStartDiagnostic(requested, rows2, "Operation trace start matched multiple indexed operations")] };
|
|
3345
3829
|
const operationId = String(rows2[0]?.operationId);
|
|
3346
3830
|
const impl = implementationScope(db, operationId);
|
|
3347
|
-
if (impl.edge?.status === "resolved" && impl.files.size > 0) return { files: impl.files, symbols: impl.symbolId ? /* @__PURE__ */ new Set([impl.symbolId]) : void 0, operationId, diagnostics: [] };
|
|
3831
|
+
if (impl.edge?.status === "resolved" && impl.files.size > 0) return { files: impl.files, symbols: impl.symbolId ? /* @__PURE__ */ new Set([impl.symbolId]) : void 0, repoId: impl.repoId, operationId, diagnostics: [] };
|
|
3348
3832
|
const hinted = implementationMethodIdFromHint(impl.edge, hintOptions);
|
|
3349
3833
|
if (hinted.methodId) {
|
|
3350
3834
|
const hintedScope = handlerScope(db, hinted.methodId);
|
|
3351
|
-
if (hintedScope?.files.size) return { files: hintedScope.files, symbols: hintedScope.symbolId ? /* @__PURE__ */ new Set([hintedScope.symbolId]) : void 0, operationId, diagnostics: [] };
|
|
3835
|
+
if (hintedScope?.files.size) return { files: hintedScope.files, symbols: hintedScope.symbolId ? /* @__PURE__ */ new Set([hintedScope.symbolId]) : void 0, repoId: hintedScope.repoId, operationId, diagnostics: [] };
|
|
3352
3836
|
}
|
|
3353
3837
|
if (impl.edge) {
|
|
3354
3838
|
const evidence = parseEvidence(impl.edge.evidence_json);
|
|
3355
3839
|
const hintDiagnostic = implementationHintDiagnostic(hinted, evidence.implementationHintSuggestions);
|
|
3356
|
-
const diagnostics = [{ severity: "warning", code: impl.edge.status === "ambiguous" ? "trace_start_ambiguous" : "trace_start_implementation_unresolved", message: `Indexed operation matched but implementation edge is ${String(impl.edge.status ?? "unresolved")}`, resolutionStage: "implementation", resolutionStatus: impl.edge.status === "ambiguous" ? "ambiguous_implementation" : "rejected_implementation", implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, implementationAmbiguityReasons: evidence.ambiguityReasons, implementationHintSuggestions: evidence.implementationHintSuggestions, candidates: evidence.candidates }];
|
|
3840
|
+
const diagnostics = [{ severity: "warning", code: impl.edge.status === "ambiguous" ? "trace_start_ambiguous" : "trace_start_implementation_unresolved", message: `Indexed operation matched but implementation edge is ${String(impl.edge.status ?? "unresolved")}`, resolutionStage: "implementation", resolutionStatus: impl.edge.status === "ambiguous" ? "ambiguous_implementation" : "rejected_implementation", implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, implementationAmbiguityReasons: evidence.ambiguityReasons, implementationRejectionReasons: implementationRejectionReasons(evidence), implementationHintSuggestions: evidence.implementationHintSuggestions, candidates: evidence.candidates }];
|
|
3357
3841
|
return { operationId, diagnostics: hintDiagnostic ? [hintDiagnostic, ...diagnostics] : diagnostics };
|
|
3358
3842
|
}
|
|
3359
3843
|
return { operationId, diagnostics: [{ severity: "warning", code: "trace_start_implementation_unresolved", message: "Indexed operation matched but no implementation candidate exists", resolutionStage: "implementation", resolutionStatus: "operation_without_implementation" }] };
|
|
3360
3844
|
}
|
|
3845
|
+
function implementationRejectionReasons(evidence) {
|
|
3846
|
+
const candidates = Array.isArray(evidence.candidates) ? evidence.candidates.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
|
|
3847
|
+
const reasons = candidates.flatMap((candidate) => Array.isArray(candidate.rejectedReasons) ? candidate.rejectedReasons.filter((reason) => typeof reason === "string") : []);
|
|
3848
|
+
return [...new Set(reasons)].sort();
|
|
3849
|
+
}
|
|
3361
3850
|
function sourceFilesForStart(db, repoId, start) {
|
|
3362
3851
|
const handler = start.handler;
|
|
3363
3852
|
const operation = normalizeOperation(start.operation ?? start.operationPath);
|
|
@@ -3385,7 +3874,7 @@ function sourceFilesForStart(db, repoId, start) {
|
|
|
3385
3874
|
operation,
|
|
3386
3875
|
operation
|
|
3387
3876
|
);
|
|
3388
|
-
if (rows2.length > 0) return { files: new Set(rows2.map((row) => row.sourceFile).filter(Boolean)), symbols: new Set(rows2.map((row) => Number(row.symbolId)).filter(Boolean)) };
|
|
3877
|
+
if (rows2.length > 0) return { files: new Set(rows2.map((row) => row.sourceFile).filter(Boolean)), symbols: new Set(rows2.map((row) => Number(row.symbolId)).filter(Boolean)), repoId };
|
|
3389
3878
|
if (start.servicePath && operation) {
|
|
3390
3879
|
const implRows = db.prepare(`SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId
|
|
3391
3880
|
FROM cds_services s JOIN cds_operations o ON o.service_id=s.id
|
|
@@ -3394,7 +3883,7 @@ function sourceFilesForStart(db, repoId, start) {
|
|
|
3394
3883
|
JOIN handler_classes hc ON hc.id=hm.handler_class_id
|
|
3395
3884
|
LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id AND sym.source_file=hc.source_file AND sym.name=hm.method_name
|
|
3396
3885
|
WHERE (? IS NULL OR s.repo_id=?) AND s.service_path=? AND (o.operation_path=? OR o.operation_name=?)`).all(repoId, repoId, start.servicePath, operation, operation);
|
|
3397
|
-
if (implRows.length > 0) return { files: new Set(implRows.map((row) => row.sourceFile).filter(Boolean)), symbols: new Set(implRows.map((row) => Number(row.symbolId)).filter(Boolean)) };
|
|
3886
|
+
if (implRows.length > 0) return { files: new Set(implRows.map((row) => row.sourceFile).filter(Boolean)), symbols: new Set(implRows.map((row) => Number(row.symbolId)).filter(Boolean)), repoId };
|
|
3398
3887
|
}
|
|
3399
3888
|
return void 0;
|
|
3400
3889
|
}
|
|
@@ -3414,6 +3903,7 @@ function startScope(db, start, hintOptions) {
|
|
|
3414
3903
|
return { repo, selectorMatched: false };
|
|
3415
3904
|
return {
|
|
3416
3905
|
repo,
|
|
3906
|
+
executionRepoId: sourceScope?.repoId ?? repo?.id,
|
|
3417
3907
|
sourceFiles,
|
|
3418
3908
|
symbolIds: sourceScope?.symbols,
|
|
3419
3909
|
selectorMatched: !terminalOperationScope && (!hasSelector || sourceFiles !== void 0),
|
|
@@ -3560,20 +4050,59 @@ function knownBindingsForCalls(db, calls) {
|
|
|
3560
4050
|
function knownBindingsForScope(db, repoId, symbolIds, files) {
|
|
3561
4051
|
const map = /* @__PURE__ */ new Map();
|
|
3562
4052
|
if (repoId === void 0) return map;
|
|
3563
|
-
const rows2 = db.prepare(`SELECT b.id,b.variable_name variableName,b.alias,b.alias_expr aliasExpr,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.source_file sourceFile,b.source_line sourceLine,req.service_path requireServicePath,req.destination requireDestination
|
|
4053
|
+
const rows2 = db.prepare(`SELECT b.id,b.symbol_id symbolId,b.variable_name variableName,b.alias,b.alias_expr aliasExpr,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.source_file sourceFile,b.source_line sourceLine,req.service_path requireServicePath,req.destination requireDestination
|
|
3564
4054
|
FROM service_bindings b LEFT JOIN cds_requires req ON req.repo_id=b.repo_id AND req.alias=b.alias
|
|
3565
4055
|
WHERE b.repo_id=?`).all(repoId);
|
|
3566
4056
|
for (const row of rows2) {
|
|
3567
4057
|
if (!row.variableName) continue;
|
|
3568
4058
|
if (files && !files.has(String(row.sourceFile))) continue;
|
|
3569
|
-
if (symbolIds && symbolIds.size > 0)
|
|
3570
|
-
|
|
3571
|
-
|
|
4059
|
+
if (symbolIds && symbolIds.size > 0 && row.symbolId != null && !symbolIds.has(Number(row.symbolId))) continue;
|
|
4060
|
+
const candidate = enrichBinding({
|
|
4061
|
+
...row,
|
|
4062
|
+
bindingId: Number(row.id),
|
|
4063
|
+
source: "local_service_binding",
|
|
4064
|
+
calleeReceiver: row.variableName,
|
|
4065
|
+
resolutionStatus: "selected"
|
|
4066
|
+
});
|
|
4067
|
+
const existing = map.get(row.variableName);
|
|
4068
|
+
if (!existing) {
|
|
4069
|
+
map.set(row.variableName, candidate);
|
|
4070
|
+
continue;
|
|
3572
4071
|
}
|
|
3573
|
-
|
|
4072
|
+
const candidates = uniqueBindingCandidates([
|
|
4073
|
+
...existing.bindingCandidates ?? [bindingCandidateEvidence(existing)],
|
|
4074
|
+
bindingCandidateEvidence(candidate)
|
|
4075
|
+
]);
|
|
4076
|
+
map.set(row.variableName, {
|
|
4077
|
+
...candidate,
|
|
4078
|
+
bindingId: void 0,
|
|
4079
|
+
source: "ambiguous_local_service_bindings",
|
|
4080
|
+
resolutionStatus: "ambiguous",
|
|
4081
|
+
bindingCandidates: candidates
|
|
4082
|
+
});
|
|
3574
4083
|
}
|
|
3575
4084
|
return map;
|
|
3576
4085
|
}
|
|
4086
|
+
function bindingCandidateEvidence(binding) {
|
|
4087
|
+
return {
|
|
4088
|
+
bindingId: binding.bindingId,
|
|
4089
|
+
sourceFile: binding.sourceFile,
|
|
4090
|
+
sourceLine: binding.sourceLine,
|
|
4091
|
+
alias: binding.alias,
|
|
4092
|
+
aliasExpr: binding.aliasExpr,
|
|
4093
|
+
destinationExpr: binding.destinationExpr,
|
|
4094
|
+
servicePathExpr: binding.servicePathExpr
|
|
4095
|
+
};
|
|
4096
|
+
}
|
|
4097
|
+
function uniqueBindingCandidates(candidates) {
|
|
4098
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4099
|
+
return candidates.filter((candidate) => {
|
|
4100
|
+
const key = JSON.stringify(candidate);
|
|
4101
|
+
if (seen.has(key)) return false;
|
|
4102
|
+
seen.add(key);
|
|
4103
|
+
return true;
|
|
4104
|
+
});
|
|
4105
|
+
}
|
|
3577
4106
|
function contextForSymbolCall(db, symbolCall, callerBindings) {
|
|
3578
4107
|
const next = /* @__PURE__ */ new Map();
|
|
3579
4108
|
if (callerBindings.size === 0) return next;
|
|
@@ -3623,6 +4152,21 @@ function contextForSymbolCall(db, symbolCall, callerBindings) {
|
|
|
3623
4152
|
}
|
|
3624
4153
|
function contextualRuntimeResolution(db, call, binding, workspaceId, persistedRows = []) {
|
|
3625
4154
|
if (!binding || String(call.call_type) !== "remote_action" || call.operation_path_expr === void 0 || call.operation_path_expr === null) return {};
|
|
4155
|
+
if (binding.resolutionStatus === "ambiguous") {
|
|
4156
|
+
return {
|
|
4157
|
+
evidence: {
|
|
4158
|
+
contextualServiceBindingAttempted: true,
|
|
4159
|
+
contextualBinding: {
|
|
4160
|
+
source: binding.source,
|
|
4161
|
+
status: "tied",
|
|
4162
|
+
candidates: binding.bindingCandidates
|
|
4163
|
+
},
|
|
4164
|
+
contextualResolutionStatus: "ambiguous",
|
|
4165
|
+
contextualCandidateCount: binding.bindingCandidates?.length ?? 0
|
|
4166
|
+
},
|
|
4167
|
+
unresolvedReason: "Ambiguous contextual service binding candidates"
|
|
4168
|
+
};
|
|
4169
|
+
}
|
|
3626
4170
|
const normalized = normalizeODataOperationInvocationPath(String(call.operation_path_expr));
|
|
3627
4171
|
const op = normalized?.normalizedOperationPath ?? (String(call.operation_path_expr).startsWith("/") ? String(call.operation_path_expr) : `/${String(call.operation_path_expr)}`);
|
|
3628
4172
|
const servicePath = binding.effectiveServicePath ?? binding.servicePathExpr ?? binding.requireServicePath;
|
|
@@ -3655,7 +4199,7 @@ function trace(db, start, options) {
|
|
|
3655
4199
|
const edges = [];
|
|
3656
4200
|
const nodes = /* @__PURE__ */ new Map();
|
|
3657
4201
|
const seenEdges = /* @__PURE__ */ new Set();
|
|
3658
|
-
const queue = scope.selectorMatched ? [{ repoId: scope.
|
|
4202
|
+
const queue = scope.selectorMatched ? [{ repoId: scope.executionRepoId, files: scope.sourceFiles, symbolIds: scope.symbolIds, depth: 1, context: /* @__PURE__ */ new Map() }] : [];
|
|
3659
4203
|
if (scope.startOperationId && scope.selectorMatched) {
|
|
3660
4204
|
const op = operationNode(db, scope.startOperationId);
|
|
3661
4205
|
const impl = implementationScope(db, scope.startOperationId);
|
|
@@ -3725,7 +4269,7 @@ function trace(db, start, options) {
|
|
|
3725
4269
|
String(row.evidence_json || "{}")
|
|
3726
4270
|
);
|
|
3727
4271
|
const rawEvidence = baseTraceEvidence(row, call, persistedEvidence, contextual.evidence);
|
|
3728
|
-
const effective = runtimeResolution(db, row, rawEvidence, options.vars, workspaceIdForCall(db, String(call.id)));
|
|
4272
|
+
const effective = runtimeResolution(db, row, rawEvidence, options.vars, workspaceIdForCall(db, String(call.id)), contextual.unresolvedReason);
|
|
3729
4273
|
const evidence = effective.evidence;
|
|
3730
4274
|
const effectiveRow = effective.row;
|
|
3731
4275
|
const targetNode = `${effectiveRow.to_kind}:${effectiveRow.to_id}`;
|
|
@@ -3824,6 +4368,7 @@ export {
|
|
|
3824
4368
|
parseImplementationHint,
|
|
3825
4369
|
implementationHintSuggestions,
|
|
3826
4370
|
linkWorkspace,
|
|
4371
|
+
parseVars,
|
|
3827
4372
|
trace
|
|
3828
4373
|
};
|
|
3829
|
-
//# sourceMappingURL=chunk-
|
|
4374
|
+
//# sourceMappingURL=chunk-52OUS3MO.js.map
|