@saptools/service-flow 0.1.43 → 0.1.45
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 -1
- package/README.md +9 -1
- package/dist/{chunk-KHQK7CFH.js → chunk-BXSCE5CR.js} +1978 -1510
- package/dist/chunk-BXSCE5CR.js.map +1 -0
- package/dist/cli.js +471 -476
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/doctor.ts +435 -0
- package/src/cli.ts +18 -453
- package/src/index.ts +2 -0
- package/src/indexer/repository-indexer.ts +52 -26
- package/src/indexer/workspace-indexer.ts +17 -5
- package/src/linker/cross-repo-linker.ts +45 -3
- package/src/linker/odata-path-normalizer.ts +4 -1
- package/src/parsers/imported-wrapper-parser.ts +262 -0
- package/src/parsers/outbound-call-parser.ts +17 -5
- package/src/parsers/service-binding-parser-helpers.ts +160 -0
- package/src/parsers/service-binding-parser.ts +57 -242
- package/src/parsers/symbol-parser.ts +25 -10
- package/src/trace/evidence.ts +210 -0
- package/src/trace/implementation-hints.ts +151 -0
- package/src/trace/trace-engine.ts +75 -117
- package/src/types.ts +8 -0
- package/dist/chunk-KHQK7CFH.js.map +0 -1
|
@@ -577,1515 +577,1716 @@ function summarizeExpression(text) {
|
|
|
577
577
|
return redactText(text).slice(0, 240);
|
|
578
578
|
}
|
|
579
579
|
|
|
580
|
-
// src/
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
if (!raw.startsWith("/")) return rejected("path_is_not_absolute");
|
|
591
|
-
if (raw.slice(1, open).includes("/")) return rejected("operation_segment_contains_navigation_separator");
|
|
592
|
-
const close = matchingClose(raw, open);
|
|
593
|
-
if (close === void 0) return rejected("top_level_invocation_parenthesis_is_unbalanced");
|
|
594
|
-
if (raw.slice(close + 1).trim().length > 0) return rejected("top_level_invocation_does_not_cover_remaining_path");
|
|
595
|
-
const operationSegment = raw.slice(0, open).trim();
|
|
596
|
-
if (operationSegment.length <= 1) return rejected("operation_segment_is_empty");
|
|
597
|
-
return {
|
|
598
|
-
rawOperationPath: raw,
|
|
599
|
-
normalizedOperationPath: operationSegment,
|
|
600
|
-
wasInvocation: true,
|
|
601
|
-
invocationArgumentPlaceholderKeys: [...new Set(extractTemplatePlaceholders(raw.slice(open + 1, close)))],
|
|
602
|
-
normalizationReason: "balanced_top_level_operation_invocation"
|
|
603
|
-
};
|
|
580
|
+
// src/parsers/service-binding-parser.ts
|
|
581
|
+
import path8 from "path";
|
|
582
|
+
import ts5 from "typescript";
|
|
583
|
+
|
|
584
|
+
// src/parsers/service-binding-parser-helpers.ts
|
|
585
|
+
import fs6 from "fs/promises";
|
|
586
|
+
import path7 from "path";
|
|
587
|
+
import ts4 from "typescript";
|
|
588
|
+
function lineOf3(sf, node) {
|
|
589
|
+
return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
604
590
|
}
|
|
605
|
-
function
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
const
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
const
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
591
|
+
function stringValue(node) {
|
|
592
|
+
if (!node) return void 0;
|
|
593
|
+
if (ts4.isStringLiteralLike(node) || ts4.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
594
|
+
if (ts4.isTemplateExpression(node)) return node.getText().replace(/^`|`$/g, "");
|
|
595
|
+
return node.getText();
|
|
596
|
+
}
|
|
597
|
+
function placeholders(value) {
|
|
598
|
+
return [...(value ?? "").matchAll(/\$\{([^}]*)\}/g)].map((m) => (m[1] ?? "").trim()).filter(Boolean);
|
|
599
|
+
}
|
|
600
|
+
function connectFactFromCall(call) {
|
|
601
|
+
const expr = call.expression;
|
|
602
|
+
if (!ts4.isPropertyAccessExpression(expr) || expr.name.text !== "to") return void 0;
|
|
603
|
+
const inner = expr.expression;
|
|
604
|
+
if (!ts4.isPropertyAccessExpression(inner) || inner.name.text !== "connect" || inner.expression.getText() !== "cds") return void 0;
|
|
605
|
+
const first = call.arguments[0];
|
|
606
|
+
if (!first) return void 0;
|
|
607
|
+
const second = call.arguments[1];
|
|
608
|
+
const objectArg = ts4.isObjectLiteralExpression(first) ? first : second && ts4.isObjectLiteralExpression(second) ? second : void 0;
|
|
609
|
+
let alias;
|
|
610
|
+
let aliasExpr;
|
|
611
|
+
if (ts4.isStringLiteralLike(first) || ts4.isNoSubstitutionTemplateLiteral(first)) alias = first.text;
|
|
612
|
+
else if (!ts4.isObjectLiteralExpression(first)) aliasExpr = stringValue(first);
|
|
613
|
+
if ((ts4.isStringLiteralLike(first) || ts4.isNoSubstitutionTemplateLiteral(first)) && !objectArg) return { alias: first.text, isDynamic: false, placeholders: [] };
|
|
614
|
+
if (!objectArg && aliasExpr) return { aliasExpr, isDynamic: true, placeholders: placeholders(aliasExpr) };
|
|
615
|
+
const expressions = objectArg ? objectExpressions(objectArg) : {};
|
|
616
|
+
const ph = [...placeholders(aliasExpr ?? alias), ...placeholders(expressions.destinationExpr), ...placeholders(expressions.servicePathExpr)];
|
|
617
|
+
return { alias, aliasExpr, ...expressions, isDynamic: ph.length > 0 || !expressions.destinationExpr && !expressions.servicePathExpr, placeholders: ph };
|
|
618
|
+
}
|
|
619
|
+
function objectExpressions(objectArg) {
|
|
620
|
+
const out = {};
|
|
621
|
+
function visitObject(obj) {
|
|
622
|
+
for (const prop of obj.properties) {
|
|
623
|
+
if (!ts4.isPropertyAssignment(prop)) continue;
|
|
624
|
+
const name = ts4.isIdentifier(prop.name) || ts4.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
625
|
+
if (name === "destination") out.destinationExpr = stringValue(prop.initializer);
|
|
626
|
+
if (name === "path" || name === "servicePath") out.servicePathExpr = stringValue(prop.initializer);
|
|
627
|
+
if (ts4.isObjectLiteralExpression(prop.initializer)) visitObject(prop.initializer);
|
|
628
|
+
}
|
|
640
629
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
630
|
+
visitObject(objectArg);
|
|
631
|
+
return out;
|
|
632
|
+
}
|
|
633
|
+
function unwrapCall(expr) {
|
|
634
|
+
if (ts4.isAwaitExpression(expr)) return unwrapCall(expr.expression);
|
|
635
|
+
if (ts4.isParenthesizedExpression(expr)) return unwrapCall(expr.expression);
|
|
636
|
+
if (ts4.isAsExpression(expr) || ts4.isSatisfiesExpression(expr)) return unwrapCall(expr.expression);
|
|
637
|
+
if (ts4.isTypeAssertionExpression(expr)) return unwrapCall(expr.expression);
|
|
638
|
+
if (ts4.isCallExpression(expr)) return expr;
|
|
639
|
+
return void 0;
|
|
640
|
+
}
|
|
641
|
+
function unwrapIdentityExpression(expr) {
|
|
642
|
+
if (ts4.isAwaitExpression(expr)) return unwrapIdentityExpression(expr.expression);
|
|
643
|
+
if (ts4.isParenthesizedExpression(expr)) return unwrapIdentityExpression(expr.expression);
|
|
644
|
+
if (ts4.isAsExpression(expr) || ts4.isSatisfiesExpression(expr)) return unwrapIdentityExpression(expr.expression);
|
|
645
|
+
if (ts4.isTypeAssertionExpression(expr)) return unwrapIdentityExpression(expr.expression);
|
|
646
|
+
return expr;
|
|
647
|
+
}
|
|
648
|
+
function transactionReceiverName(expr) {
|
|
649
|
+
const call = unwrapCall(expr);
|
|
650
|
+
if (call && ts4.isPropertyAccessExpression(call.expression) && ["tx", "transaction"].includes(call.expression.name.text) && ts4.isIdentifier(call.expression.expression)) return call.expression.expression.text;
|
|
651
|
+
const unwrapped = unwrapIdentityExpression(expr);
|
|
652
|
+
if (!ts4.isConditionalExpression(unwrapped)) return void 0;
|
|
653
|
+
const left = transactionReceiverName(unwrapped.whenTrue);
|
|
654
|
+
const right = transactionReceiverName(unwrapped.whenFalse);
|
|
655
|
+
return left && left === right ? left : void 0;
|
|
656
|
+
}
|
|
657
|
+
function findConnectInExpression(expr) {
|
|
658
|
+
const direct = unwrapCall(expr);
|
|
659
|
+
if (direct) {
|
|
660
|
+
const fact = connectFactFromCall(direct);
|
|
661
|
+
if (fact) return fact;
|
|
645
662
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
if (
|
|
649
|
-
|
|
663
|
+
let found;
|
|
664
|
+
function visit(node) {
|
|
665
|
+
if (found) return;
|
|
666
|
+
if (ts4.isCallExpression(node)) found = connectFactFromCall(node);
|
|
667
|
+
if (!found) ts4.forEachChild(node, visit);
|
|
650
668
|
}
|
|
651
|
-
|
|
652
|
-
return
|
|
653
|
-
}
|
|
654
|
-
function entitySegmentFromPath(path9) {
|
|
655
|
-
const first = path9.replace(/^\//, "").split("/")[0]?.trim();
|
|
656
|
-
if (!first) return void 0;
|
|
657
|
-
const open = first.indexOf("(");
|
|
658
|
-
const entity = (open >= 0 ? first.slice(0, open) : first).trim();
|
|
659
|
-
return entity || void 0;
|
|
669
|
+
visit(expr);
|
|
670
|
+
return found;
|
|
660
671
|
}
|
|
661
|
-
function
|
|
662
|
-
|
|
672
|
+
async function readSource(abs) {
|
|
673
|
+
try {
|
|
674
|
+
const text = await fs6.readFile(abs, "utf8");
|
|
675
|
+
return ts4.createSourceFile(abs, text, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
|
|
676
|
+
} catch {
|
|
677
|
+
return void 0;
|
|
678
|
+
}
|
|
663
679
|
}
|
|
664
|
-
function
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
const
|
|
668
|
-
|
|
680
|
+
async function resolveImport(repoPath, fromFile, spec) {
|
|
681
|
+
if (!spec.startsWith(".")) return void 0;
|
|
682
|
+
const rawBase = path7.resolve(repoPath, path7.dirname(fromFile), spec);
|
|
683
|
+
const parsed = path7.parse(rawBase);
|
|
684
|
+
const base = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"].includes(parsed.ext) ? path7.join(parsed.dir, parsed.name) : rawBase;
|
|
685
|
+
for (const candidate of [base, `${base}.ts`, `${base}.js`, path7.join(base, "index.ts"), path7.join(base, "index.js")]) {
|
|
686
|
+
const stat = await fs6.stat(candidate).catch(() => void 0);
|
|
687
|
+
if (stat?.isFile()) return normalizePath(path7.relative(repoPath, candidate));
|
|
688
|
+
}
|
|
689
|
+
return void 0;
|
|
669
690
|
}
|
|
670
|
-
function
|
|
671
|
-
const
|
|
672
|
-
for (
|
|
673
|
-
if (
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
if (
|
|
678
|
-
|
|
691
|
+
async function importsFor(repoPath, filePath, sf) {
|
|
692
|
+
const imports = [];
|
|
693
|
+
for (const stmt of sf.statements) {
|
|
694
|
+
if (!ts4.isImportDeclaration(stmt) || !ts4.isStringLiteralLike(stmt.moduleSpecifier)) continue;
|
|
695
|
+
const sourceFile = await resolveImport(repoPath, filePath, stmt.moduleSpecifier.text);
|
|
696
|
+
const clause = stmt.importClause;
|
|
697
|
+
if (!clause) continue;
|
|
698
|
+
if (clause.name) imports.push({ localName: clause.name.text, exportedName: "default", sourceFile });
|
|
699
|
+
const bindings = clause.namedBindings;
|
|
700
|
+
if (bindings && ts4.isNamedImports(bindings))
|
|
701
|
+
for (const el of bindings.elements) imports.push({ localName: el.name.text, exportedName: el.propertyName?.text ?? el.name.text, sourceFile });
|
|
679
702
|
}
|
|
680
|
-
return
|
|
703
|
+
return imports;
|
|
681
704
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
705
|
+
|
|
706
|
+
// src/parsers/service-binding-parser.ts
|
|
707
|
+
function collectLocalBindingFacts(fn) {
|
|
708
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
709
|
+
function visit(node) {
|
|
710
|
+
if (node !== fn && (ts5.isFunctionDeclaration(node) || ts5.isArrowFunction(node) || ts5.isFunctionExpression(node)))
|
|
711
|
+
return;
|
|
712
|
+
if (ts5.isVariableDeclaration(node) && ts5.isIdentifier(node.name) && node.initializer) {
|
|
713
|
+
const fact = findConnectInExpression(node.initializer);
|
|
714
|
+
if (fact) bindings.set(node.name.text, fact);
|
|
715
|
+
const sourceName = transactionReceiverName(node.initializer);
|
|
716
|
+
if (sourceName) {
|
|
717
|
+
const source = bindings.get(sourceName);
|
|
718
|
+
if (source) bindings.set(node.name.text, { ...source, helperChain: [...source.helperChain ?? [], { aliasOf: sourceName, callerVariable: node.name.text, aliasKind: "transaction", transactionAliasSource: sourceName }] });
|
|
695
719
|
}
|
|
696
|
-
if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
|
|
697
|
-
continue;
|
|
698
|
-
}
|
|
699
|
-
if (char === "$" && text[index + 1] === "{") {
|
|
700
|
-
const close = matchingPlaceholderClose(text, index + 1);
|
|
701
|
-
if (close === void 0) return void 0;
|
|
702
|
-
index = close;
|
|
703
|
-
continue;
|
|
704
|
-
}
|
|
705
|
-
if (char === "'") {
|
|
706
|
-
quote = "single";
|
|
707
|
-
continue;
|
|
708
|
-
}
|
|
709
|
-
if (char === '"') {
|
|
710
|
-
quote = "double";
|
|
711
|
-
continue;
|
|
712
|
-
}
|
|
713
|
-
if (char === "`") {
|
|
714
|
-
quote = "template";
|
|
715
|
-
continue;
|
|
716
|
-
}
|
|
717
|
-
if (char === "(") depth += 1;
|
|
718
|
-
if (char === ")") {
|
|
719
|
-
depth -= 1;
|
|
720
|
-
if (depth === 0) return index;
|
|
721
|
-
if (depth < 0) return void 0;
|
|
722
720
|
}
|
|
721
|
+
ts5.forEachChild(node, visit);
|
|
723
722
|
}
|
|
724
|
-
|
|
723
|
+
visit(fn);
|
|
724
|
+
return bindings;
|
|
725
725
|
}
|
|
726
|
-
function
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
if (
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
726
|
+
function collectReturnedObjectBindings(fn) {
|
|
727
|
+
const bindings = collectLocalBindingFacts(fn);
|
|
728
|
+
const returns = /* @__PURE__ */ new Map();
|
|
729
|
+
function visit(node) {
|
|
730
|
+
if (node !== fn && (ts5.isFunctionDeclaration(node) || ts5.isArrowFunction(node) || ts5.isFunctionExpression(node)))
|
|
731
|
+
return;
|
|
732
|
+
if (ts5.isReturnStatement(node) && node.expression && ts5.isObjectLiteralExpression(node.expression)) {
|
|
733
|
+
for (const prop of node.expression.properties) {
|
|
734
|
+
if (ts5.isShorthandPropertyAssignment(prop)) returns.set(prop.name.text, prop.name.text);
|
|
735
|
+
if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.initializer)) {
|
|
736
|
+
const propertyName2 = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
737
|
+
if (propertyName2) returns.set(propertyName2, prop.initializer.text);
|
|
738
|
+
}
|
|
738
739
|
}
|
|
739
|
-
if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
|
|
740
|
-
continue;
|
|
741
|
-
}
|
|
742
|
-
if (char === "'") {
|
|
743
|
-
quote = "single";
|
|
744
|
-
continue;
|
|
745
|
-
}
|
|
746
|
-
if (char === '"') {
|
|
747
|
-
quote = "double";
|
|
748
|
-
continue;
|
|
749
|
-
}
|
|
750
|
-
if (char === "`") {
|
|
751
|
-
quote = "template";
|
|
752
|
-
continue;
|
|
753
|
-
}
|
|
754
|
-
if (char === "{") depth += 1;
|
|
755
|
-
if (char === "}") {
|
|
756
|
-
depth -= 1;
|
|
757
|
-
if (depth === 0) return index;
|
|
758
|
-
if (depth < 0) return void 0;
|
|
759
740
|
}
|
|
741
|
+
ts5.forEachChild(node, visit);
|
|
760
742
|
}
|
|
761
|
-
|
|
743
|
+
visit(fn);
|
|
744
|
+
const out = /* @__PURE__ */ new Map();
|
|
745
|
+
for (const [propertyName2, variableName] of returns) {
|
|
746
|
+
const fact = bindings.get(variableName);
|
|
747
|
+
if (fact) out.set(propertyName2, fact);
|
|
748
|
+
}
|
|
749
|
+
return out;
|
|
762
750
|
}
|
|
763
|
-
function
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
const prev = text[index - 1];
|
|
768
|
-
if (quote) {
|
|
769
|
-
if (prev === "\\") continue;
|
|
770
|
-
if (quote === "template" && char === "$" && text[index + 1] === "{") {
|
|
771
|
-
const close = matchingPlaceholderClose(text, index + 1);
|
|
772
|
-
if (close === void 0) return -1;
|
|
773
|
-
index = close;
|
|
774
|
-
continue;
|
|
775
|
-
}
|
|
776
|
-
if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
|
|
777
|
-
continue;
|
|
778
|
-
}
|
|
779
|
-
if (char === "$" && text[index + 1] === "{") {
|
|
780
|
-
const close = matchingPlaceholderClose(text, index + 1);
|
|
781
|
-
if (close === void 0) return -1;
|
|
782
|
-
index = close;
|
|
783
|
-
continue;
|
|
784
|
-
}
|
|
785
|
-
if (char === "'") {
|
|
786
|
-
quote = "single";
|
|
787
|
-
continue;
|
|
788
|
-
}
|
|
789
|
-
if (char === '"') {
|
|
790
|
-
quote = "double";
|
|
791
|
-
continue;
|
|
792
|
-
}
|
|
793
|
-
if (char === "`") {
|
|
794
|
-
quote = "template";
|
|
795
|
-
continue;
|
|
796
|
-
}
|
|
797
|
-
if (char === "?") return index;
|
|
798
|
-
}
|
|
799
|
-
return -1;
|
|
751
|
+
function functionLikeInitializer(expr) {
|
|
752
|
+
if (!expr) return void 0;
|
|
753
|
+
if (ts5.isArrowFunction(expr) || ts5.isFunctionExpression(expr)) return expr;
|
|
754
|
+
return void 0;
|
|
800
755
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
756
|
+
function directReturnConnectFact(fn) {
|
|
757
|
+
const localBindings = collectLocalBindingFacts(fn);
|
|
758
|
+
let returned;
|
|
759
|
+
function visit(node) {
|
|
760
|
+
if (node !== fn && (ts5.isFunctionDeclaration(node) || ts5.isArrowFunction(node) || ts5.isFunctionExpression(node)))
|
|
761
|
+
return;
|
|
762
|
+
if (!returned && ts5.isReturnStatement(node) && node.expression)
|
|
763
|
+
returned = node.expression;
|
|
764
|
+
if (!returned) ts5.forEachChild(node, visit);
|
|
765
|
+
}
|
|
766
|
+
visit(fn);
|
|
767
|
+
if (!returned) return void 0;
|
|
768
|
+
if (ts5.isIdentifier(returned)) return localBindings.get(returned.text);
|
|
769
|
+
return findConnectInExpression(returned);
|
|
812
770
|
}
|
|
813
|
-
function
|
|
814
|
-
|
|
771
|
+
function directConnectFactFromFunctionLike(fn) {
|
|
772
|
+
if (ts5.isArrowFunction(fn) && fn.body && !ts5.isBlock(fn.body))
|
|
773
|
+
return findConnectInExpression(fn.body);
|
|
774
|
+
return directReturnConnectFact(fn);
|
|
815
775
|
}
|
|
816
|
-
function
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
776
|
+
function exportedLocalNames(sf) {
|
|
777
|
+
const exports = /* @__PURE__ */ new Map();
|
|
778
|
+
for (const stmt of sf.statements) {
|
|
779
|
+
const direct = ts5.canHaveModifiers(stmt) ? ts5.getModifiers(stmt)?.some(
|
|
780
|
+
(m) => m.kind === ts5.SyntaxKind.ExportKeyword
|
|
781
|
+
) ?? false : false;
|
|
782
|
+
if (direct && ts5.isFunctionDeclaration(stmt) && stmt.name)
|
|
783
|
+
exports.set(stmt.name.text, stmt.name.text);
|
|
784
|
+
if (direct && ts5.isVariableStatement(stmt)) {
|
|
785
|
+
for (const decl of stmt.declarationList.declarations)
|
|
786
|
+
if (ts5.isIdentifier(decl.name)) exports.set(decl.name.text, decl.name.text);
|
|
787
|
+
}
|
|
788
|
+
if (!ts5.isExportDeclaration(stmt) || !stmt.exportClause) continue;
|
|
789
|
+
if (!ts5.isNamedExports(stmt.exportClause)) continue;
|
|
790
|
+
for (const el of stmt.exportClause.elements)
|
|
791
|
+
exports.set(el.name.text, el.propertyName?.text ?? el.name.text);
|
|
826
792
|
}
|
|
793
|
+
return exports;
|
|
827
794
|
}
|
|
828
|
-
function
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
const
|
|
832
|
-
const
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
795
|
+
async function helperBindings(repoPath, filePath) {
|
|
796
|
+
const sf = await readSource(path8.join(repoPath, filePath));
|
|
797
|
+
if (!sf) return [];
|
|
798
|
+
const sourceFileAst = sf;
|
|
799
|
+
const out = [];
|
|
800
|
+
const exportedLocals = exportedLocalNames(sf);
|
|
801
|
+
const factsByLocal = /* @__PURE__ */ new Map();
|
|
802
|
+
for (const stmt of sf.statements) {
|
|
803
|
+
if (ts5.isFunctionDeclaration(stmt) && stmt.name) {
|
|
804
|
+
const fact = directConnectFactFromFunctionLike(stmt);
|
|
805
|
+
if (fact) factsByLocal.set(stmt.name.text, { ...fact, sourceLine: lineOf3(sf, stmt) });
|
|
806
|
+
for (const [returnedProperty, objectFact] of collectReturnedObjectBindings(stmt))
|
|
807
|
+
factsByLocal.set(`${stmt.name.text}#${returnedProperty}`, { ...objectFact, returnedProperty, sourceLine: lineOf3(sf, stmt) });
|
|
808
|
+
}
|
|
809
|
+
if (ts5.isVariableStatement(stmt))
|
|
810
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
811
|
+
if (!ts5.isIdentifier(decl.name) || !decl.initializer) continue;
|
|
812
|
+
const helper = functionLikeInitializer(decl.initializer);
|
|
813
|
+
if (helper) {
|
|
814
|
+
const directReturn = directConnectFactFromFunctionLike(helper);
|
|
815
|
+
if (directReturn)
|
|
816
|
+
factsByLocal.set(decl.name.text, {
|
|
817
|
+
...directReturn,
|
|
818
|
+
sourceLine: lineOf3(sourceFileAst, decl)
|
|
819
|
+
});
|
|
820
|
+
for (const [returnedProperty, objectFact] of collectReturnedObjectBindings(helper))
|
|
821
|
+
factsByLocal.set(`${decl.name.text}#${returnedProperty}`, {
|
|
822
|
+
...objectFact,
|
|
823
|
+
returnedProperty,
|
|
824
|
+
sourceLine: lineOf3(sourceFileAst, decl)
|
|
825
|
+
});
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
const fact = findConnectInExpression(decl.initializer);
|
|
829
|
+
if (fact)
|
|
830
|
+
factsByLocal.set(decl.name.text, {
|
|
831
|
+
...fact,
|
|
832
|
+
sourceLine: lineOf3(sourceFileAst, decl)
|
|
833
|
+
});
|
|
834
|
+
}
|
|
838
835
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
836
|
+
for (const [exportedName, localName] of exportedLocals) {
|
|
837
|
+
const fact = factsByLocal.get(localName);
|
|
838
|
+
if (fact)
|
|
839
|
+
out.push({
|
|
840
|
+
...fact,
|
|
841
|
+
exportedName,
|
|
842
|
+
sourceFile: normalizePath(filePath),
|
|
843
|
+
sourceLine: fact.sourceLine
|
|
844
|
+
});
|
|
843
845
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
} catch {
|
|
852
|
-
return {};
|
|
846
|
+
for (const [key, fact] of factsByLocal) {
|
|
847
|
+
const [localName, returnedProperty] = key.split("#");
|
|
848
|
+
if (!returnedProperty) continue;
|
|
849
|
+
for (const [exportedName, exportedLocal] of exportedLocals) {
|
|
850
|
+
if (exportedLocal !== localName) continue;
|
|
851
|
+
out.push({ ...fact, exportedName, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: fact.sourceLine });
|
|
852
|
+
}
|
|
853
853
|
}
|
|
854
|
+
return out;
|
|
854
855
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
if (
|
|
876
|
-
|
|
877
|
-
|
|
856
|
+
async function parseServiceBindings(repoPath, filePath) {
|
|
857
|
+
const sf = await readSource(path8.join(repoPath, filePath));
|
|
858
|
+
if (!sf) return [];
|
|
859
|
+
const sourceFileAst = sf;
|
|
860
|
+
const out = [];
|
|
861
|
+
const imports = await importsFor(repoPath, filePath, sf);
|
|
862
|
+
const helperCache = /* @__PURE__ */ new Map();
|
|
863
|
+
const classHelpers = collectClassHelpers(sourceFileAst);
|
|
864
|
+
const localObjectHelpers = /* @__PURE__ */ new Map();
|
|
865
|
+
const localDirectHelpers = /* @__PURE__ */ new Map();
|
|
866
|
+
const objectHelperVariables = /* @__PURE__ */ new Map();
|
|
867
|
+
for (const stmt of sourceFileAst.statements) {
|
|
868
|
+
if (ts5.isFunctionDeclaration(stmt) && stmt.name) {
|
|
869
|
+
const directFact = directConnectFactFromFunctionLike(stmt);
|
|
870
|
+
if (directFact) localDirectHelpers.set(stmt.name.text, { ...directFact, exportedName: stmt.name.text, sourceFile: normalizePath(filePath), sourceLine: lineOf3(sourceFileAst, stmt) });
|
|
871
|
+
const rows2 = [];
|
|
872
|
+
for (const [returnedProperty, fact] of collectReturnedObjectBindings(stmt))
|
|
873
|
+
rows2.push({ ...fact, exportedName: stmt.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf3(sourceFileAst, stmt) });
|
|
874
|
+
if (rows2.length > 0) localObjectHelpers.set(stmt.name.text, rows2);
|
|
875
|
+
}
|
|
876
|
+
if (ts5.isVariableStatement(stmt)) {
|
|
877
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
878
|
+
if (!ts5.isIdentifier(decl.name)) continue;
|
|
879
|
+
const helper = functionLikeInitializer(decl.initializer);
|
|
880
|
+
if (!helper) continue;
|
|
881
|
+
const directFact = directConnectFactFromFunctionLike(helper);
|
|
882
|
+
if (directFact) localDirectHelpers.set(decl.name.text, { ...directFact, exportedName: decl.name.text, sourceFile: normalizePath(filePath), sourceLine: lineOf3(sourceFileAst, decl) });
|
|
883
|
+
const rows2 = [];
|
|
884
|
+
for (const [returnedProperty, fact] of collectReturnedObjectBindings(helper))
|
|
885
|
+
rows2.push({ ...fact, exportedName: decl.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf3(sourceFileAst, decl) });
|
|
886
|
+
if (rows2.length > 0) localObjectHelpers.set(decl.name.text, rows2);
|
|
887
|
+
}
|
|
878
888
|
}
|
|
879
889
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
if (name === "UPDATE") return entityFromExpression(expr.arguments[0]);
|
|
890
|
-
const receiver = ts4.isPropertyAccessExpression(expr.expression) ? expr.expression.expression : void 0;
|
|
891
|
-
if (receiver) return queryEntityFromAst(receiver, initializers);
|
|
890
|
+
async function importedHelpers(localName) {
|
|
891
|
+
const imp = imports.find((i) => i.localName === localName && i.sourceFile);
|
|
892
|
+
if (!imp?.sourceFile) return [];
|
|
893
|
+
if (!helperCache.has(imp.sourceFile))
|
|
894
|
+
helperCache.set(
|
|
895
|
+
imp.sourceFile,
|
|
896
|
+
await helperBindings(repoPath, imp.sourceFile)
|
|
897
|
+
);
|
|
898
|
+
return (helperCache.get(imp.sourceFile) ?? []).filter((h) => h.exportedName === imp.exportedName).map((helper) => ({ imp, helper }));
|
|
892
899
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
function extractQueryEntity(expr) {
|
|
896
|
-
const source = ts4.createSourceFile("query.ts", `const __query = (${expr});`, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
|
|
897
|
-
const initializers = variableInitializers(source);
|
|
898
|
-
let found;
|
|
899
|
-
const visit = (node) => {
|
|
900
|
-
if (found) return;
|
|
901
|
-
if (ts4.isParenthesizedExpression(node)) found = queryEntityFromAst(node.expression, initializers);
|
|
902
|
-
ts4.forEachChild(node, visit);
|
|
903
|
-
};
|
|
904
|
-
visit(source);
|
|
905
|
-
return found;
|
|
906
|
-
}
|
|
907
|
-
function queryWarning(expr) {
|
|
908
|
-
if (/^\s*[`'"]/.test(expr)) return "raw_sql_or_cql_expression";
|
|
909
|
-
if (/^\s*\w+\s*$/.test(expr)) return "query_variable_without_static_initializer";
|
|
910
|
-
return "dynamic_entity_expression";
|
|
911
|
-
}
|
|
912
|
-
function parserEvidence(source, node, extra) {
|
|
913
|
-
return { parser: "typescript_ast", startOffset: node.getStart(source), endOffset: node.getEnd(), ...extra };
|
|
914
|
-
}
|
|
915
|
-
function isStringLike(expr) {
|
|
916
|
-
return Boolean(expr && (ts4.isStringLiteral(expr) || ts4.isNoSubstitutionTemplateLiteral(expr)));
|
|
917
|
-
}
|
|
918
|
-
function literalText(expr) {
|
|
919
|
-
if (isStringLike(expr)) return expr.text;
|
|
920
|
-
return void 0;
|
|
921
|
-
}
|
|
922
|
-
function objectPropertyText(object, key) {
|
|
923
|
-
const prop = object.properties.find(
|
|
924
|
-
(property) => ts4.isPropertyAssignment(property) && nameOfProperty(property.name) === key || ts4.isShorthandPropertyAssignment(property) && property.name.text === key
|
|
925
|
-
);
|
|
926
|
-
if (!prop) return void 0;
|
|
927
|
-
return ts4.isShorthandPropertyAssignment(prop) ? prop.name.text : prop.initializer.getText();
|
|
928
|
-
}
|
|
929
|
-
function objectPropertyIsShorthand(object, key) {
|
|
930
|
-
return object.properties.some((property) => ts4.isShorthandPropertyAssignment(property) && property.name.text === key);
|
|
931
|
-
}
|
|
932
|
-
function nameOfProperty(name) {
|
|
933
|
-
if (ts4.isIdentifier(name) || ts4.isStringLiteral(name) || ts4.isNumericLiteral(name)) return name.text;
|
|
934
|
-
return void 0;
|
|
935
|
-
}
|
|
936
|
-
var maxAliasDepth = 5;
|
|
937
|
-
function safeRaw(expr) {
|
|
938
|
-
if (ts4.isStringLiteral(expr) || ts4.isNoSubstitutionTemplateLiteral(expr) || ts4.isIdentifier(expr) || ts4.isTemplateExpression(expr)) return expr.getText(expr.getSourceFile());
|
|
939
|
-
return void 0;
|
|
940
|
-
}
|
|
941
|
-
function placeholders(expr) {
|
|
942
|
-
return expr.templateSpans.map((span) => span.expression.getText(expr.getSourceFile()));
|
|
943
|
-
}
|
|
944
|
-
function isFunctionLikeScope(node) {
|
|
945
|
-
return ts4.isFunctionLike(node) || ts4.isSourceFile(node);
|
|
946
|
-
}
|
|
947
|
-
function nodeContains(parent, child) {
|
|
948
|
-
const source = child.getSourceFile();
|
|
949
|
-
return child.getStart(source) >= parent.getStart(source) && child.getEnd() <= parent.getEnd();
|
|
950
|
-
}
|
|
951
|
-
function declarationScope(node) {
|
|
952
|
-
if (ts4.isParameter(node)) return node.parent;
|
|
953
|
-
if (ts4.isCatchClause(node.parent) && node.parent.variableDeclaration === node) return node.parent;
|
|
954
|
-
const list = node.parent;
|
|
955
|
-
const blockScoped = (list.flags & (ts4.NodeFlags.Const | ts4.NodeFlags.Let)) !== 0;
|
|
956
|
-
let current = list.parent;
|
|
957
|
-
if (!blockScoped) {
|
|
958
|
-
while (current.parent && !isFunctionLikeScope(current)) current = current.parent;
|
|
959
|
-
return current;
|
|
900
|
+
async function importedHelper(localName) {
|
|
901
|
+
return (await importedHelpers(localName)).find((row) => !row.helper.returnedProperty);
|
|
960
902
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
function isLoopInitializerScope(declaration, scope) {
|
|
965
|
-
const list = declaration.parent;
|
|
966
|
-
return ts4.isForStatement(scope) && scope.initializer === list || (ts4.isForInStatement(scope) || ts4.isForOfStatement(scope)) && scope.initializer === list;
|
|
967
|
-
}
|
|
968
|
-
function catchBindingScope(declaration) {
|
|
969
|
-
if (ts4.isParameter(declaration)) return void 0;
|
|
970
|
-
return ts4.isCatchClause(declaration.parent) && declaration.parent.variableDeclaration === declaration ? declaration.parent : void 0;
|
|
971
|
-
}
|
|
972
|
-
function isAccessibleDeclaration(declaration, use) {
|
|
973
|
-
const source = use.getSourceFile();
|
|
974
|
-
if (declaration.name.getStart(source) >= use.getStart(source)) return false;
|
|
975
|
-
const catchScope = catchBindingScope(declaration);
|
|
976
|
-
if (catchScope) return nodeContains(catchScope.block, use);
|
|
977
|
-
const scope = declarationScope(declaration);
|
|
978
|
-
if (ts4.isForStatement(scope) || ts4.isForInStatement(scope) || ts4.isForOfStatement(scope)) return nodeContains(scope.statement, use);
|
|
979
|
-
return ts4.isSourceFile(scope) || nodeContains(scope, use);
|
|
980
|
-
}
|
|
981
|
-
function resolveBinding(identifier, use) {
|
|
982
|
-
const source = use.getSourceFile();
|
|
983
|
-
let best;
|
|
984
|
-
const visit = (node) => {
|
|
985
|
-
if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
|
|
986
|
-
if (ts4.isParameter(node) && ts4.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
|
|
987
|
-
ts4.forEachChild(node, visit);
|
|
988
|
-
};
|
|
989
|
-
visit(source);
|
|
990
|
-
if (!best) return { immutable: false, evidence: ["binding_not_found"] };
|
|
991
|
-
const immutable = ts4.isVariableDeclaration(best) && (best.parent.flags & ts4.NodeFlags.Const) !== 0;
|
|
992
|
-
return { declaration: best, initializer: ts4.isVariableDeclaration(best) ? best.initializer : void 0, immutable, evidence: [immutable ? "lexical_const_binding_before_use" : "lexical_mutable_or_parameter_binding"] };
|
|
993
|
-
}
|
|
994
|
-
function resolveExpression(expr, use, policy, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
995
|
-
if (!expr) return { status: "unknown", sourceKind: "dynamic_expression", placeholderKeys: [], evidence: ["expression_missing"] };
|
|
996
|
-
if (ts4.isStringLiteral(expr)) return { status: "static", sourceKind: "string_literal", value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["string_literal"] };
|
|
997
|
-
if (ts4.isNoSubstitutionTemplateLiteral(expr)) return { status: "static", sourceKind: "no_substitution_template", value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["no_substitution_template"] };
|
|
998
|
-
if (ts4.isTemplateExpression(expr)) {
|
|
999
|
-
const keys = placeholders(expr);
|
|
1000
|
-
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"] };
|
|
1001
|
-
return { status: "dynamic", sourceKind: "template_with_substitutions", placeholderKeys: keys, evidence: ["template_substitutions_not_static_external_target"] };
|
|
903
|
+
function bindingForVariable(variableName) {
|
|
904
|
+
const sourceFile = normalizePath(filePath);
|
|
905
|
+
return [...out].reverse().find((row) => row.variableName === variableName && row.sourceFile === sourceFile);
|
|
1002
906
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
907
|
+
function cloneAliasBinding(targetName, sourceName, aliasKind, node) {
|
|
908
|
+
const existing = bindingForVariable(sourceName);
|
|
909
|
+
if (!existing) return;
|
|
910
|
+
out.push({
|
|
911
|
+
...existing,
|
|
912
|
+
variableName: targetName,
|
|
913
|
+
sourceLine: lineOf3(sourceFileAst, node),
|
|
914
|
+
helperChain: [
|
|
915
|
+
...existing.helperChain ?? [],
|
|
916
|
+
{
|
|
917
|
+
callerVariable: targetName,
|
|
918
|
+
aliasOf: sourceName,
|
|
919
|
+
aliasKind,
|
|
920
|
+
scopeRule: "same-file-source-order",
|
|
921
|
+
...aliasKind === "transaction" ? { transactionAliasSource: sourceName } : {}
|
|
922
|
+
}
|
|
923
|
+
]
|
|
924
|
+
});
|
|
1011
925
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
if (ts4.isIdentifier(expr) && initializers.has(expr.text)) return staticExpressionText(initializers.get(expr.text), initializers);
|
|
1018
|
-
return void 0;
|
|
1019
|
-
}
|
|
1020
|
-
function staticPathExpression(expr, use) {
|
|
1021
|
-
const resolution = resolveExpression(expr, use, "operation_path");
|
|
1022
|
-
const sourceKind = resolution.sourceKind === "const_alias" ? "const" : resolution.sourceKind === "template_with_substitutions" || resolution.sourceKind === "no_substitution_template" ? "template" : resolution.status === "static" ? "literal" : "dynamic";
|
|
1023
|
-
return { text: resolution.value, sourceKind, rawExpression: resolution.rawExpression, constName: resolution.constName, resolution };
|
|
1024
|
-
}
|
|
1025
|
-
function operationPathFromStatic(text) {
|
|
1026
|
-
return text ? `/${stripQuotes(text).replace(/^\//, "")}` : void 0;
|
|
1027
|
-
}
|
|
1028
|
-
function staticPathCandidates(identifier, call, method) {
|
|
1029
|
-
const binding = resolveBinding(identifier, call);
|
|
1030
|
-
const declaration = binding.declaration;
|
|
1031
|
-
if (!declaration) return void 0;
|
|
1032
|
-
const source = call.getSourceFile();
|
|
1033
|
-
const paths = [];
|
|
1034
|
-
let hasDynamicAssignments = false;
|
|
1035
|
-
const addExpr = (expr, origin) => {
|
|
1036
|
-
const resolved2 = staticPathExpression(expr, origin);
|
|
1037
|
-
if (resolved2.text) paths.push(operationPathFromStatic(resolved2.text) ?? resolved2.text);
|
|
1038
|
-
else hasDynamicAssignments = true;
|
|
1039
|
-
};
|
|
1040
|
-
if (ts4.isVariableDeclaration(declaration)) addExpr(declaration.initializer, declaration);
|
|
1041
|
-
const visit = (node) => {
|
|
1042
|
-
if (node !== source && isFunctionLikeScope(node) && !nodeContains(node, call)) return;
|
|
1043
|
-
if (node.getStart(source) >= call.getStart(source)) return;
|
|
1044
|
-
if (ts4.isBinaryExpression(node) && node.operatorToken.kind === ts4.SyntaxKind.EqualsToken && ts4.isIdentifier(node.left)) {
|
|
1045
|
-
const target = resolveBinding(node.left, node);
|
|
1046
|
-
if (target.declaration === declaration) addExpr(node.right, node);
|
|
1047
|
-
}
|
|
1048
|
-
ts4.forEachChild(node, visit);
|
|
1049
|
-
};
|
|
1050
|
-
visit(source);
|
|
1051
|
-
const candidatePaths = [...new Set(paths)];
|
|
1052
|
-
if (candidatePaths.length === 0) return void 0;
|
|
1053
|
-
const normalizedCandidateOperations = [...new Set(candidatePaths.map((candidate) => classifyODataPathIntent(candidate, method).topLevelOperationName).filter((value) => Boolean(value)))];
|
|
1054
|
-
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 };
|
|
1055
|
-
}
|
|
1056
|
-
function destinationExpressionShape(expr) {
|
|
1057
|
-
if (!expr) return void 0;
|
|
1058
|
-
if (ts4.isIdentifier(expr)) return "identifier";
|
|
1059
|
-
if (ts4.isPropertyAccessExpression(expr) || ts4.isElementAccessExpression(expr)) return "property_read";
|
|
1060
|
-
if (ts4.isCallExpression(expr)) return "function_call";
|
|
1061
|
-
if (ts4.isConditionalExpression(expr)) return "conditional";
|
|
1062
|
-
if (ts4.isBinaryExpression(expr)) return "binary_expression";
|
|
1063
|
-
if (ts4.isTemplateExpression(expr)) return "template_expression";
|
|
1064
|
-
return ts4.SyntaxKind[expr.kind] ?? "expression";
|
|
1065
|
-
}
|
|
1066
|
-
function staticConditionalCandidates(expr, initializers) {
|
|
1067
|
-
const resolved2 = expr && ts4.isIdentifier(expr) && initializers.has(expr.text) ? initializers.get(expr.text) : expr;
|
|
1068
|
-
if (!resolved2 || !ts4.isConditionalExpression(resolved2)) return void 0;
|
|
1069
|
-
const left = staticExpressionText(resolved2.whenTrue, initializers);
|
|
1070
|
-
const right = staticExpressionText(resolved2.whenFalse, initializers);
|
|
1071
|
-
if (!left || !right) return void 0;
|
|
1072
|
-
return [.../* @__PURE__ */ new Set([left, right])];
|
|
1073
|
-
}
|
|
1074
|
-
function propertyInitializer(object, key) {
|
|
1075
|
-
for (const property of object.properties) {
|
|
1076
|
-
if (ts4.isPropertyAssignment(property) && nameOfProperty(property.name) === key) return property.initializer;
|
|
1077
|
-
if (ts4.isShorthandPropertyAssignment(property) && property.name.text === key) return property.name;
|
|
926
|
+
function recordIdentityAlias(decl) {
|
|
927
|
+
if (!ts5.isIdentifier(decl.name) || !decl.initializer) return;
|
|
928
|
+
const unwrapped = unwrapIdentityExpression(decl.initializer);
|
|
929
|
+
if (!ts5.isIdentifier(unwrapped)) return;
|
|
930
|
+
cloneAliasBinding(decl.name.text, unwrapped.text, "identity", decl);
|
|
1078
931
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
932
|
+
async function recordBindingFromExpression(targetName, expr, node, aliasKind) {
|
|
933
|
+
const call = unwrapCall(expr);
|
|
934
|
+
if (!call) return;
|
|
935
|
+
const direct = connectFactFromCall(call);
|
|
936
|
+
if (direct)
|
|
937
|
+
out.push({
|
|
938
|
+
variableName: targetName,
|
|
939
|
+
...direct,
|
|
940
|
+
sourceFile: normalizePath(filePath),
|
|
941
|
+
sourceLine: lineOf3(sourceFileAst, node),
|
|
942
|
+
helperChain: aliasKind === "assignment" ? [{ callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind, scopeRule: "same-file-source-order" }] : void 0
|
|
943
|
+
});
|
|
944
|
+
else if (ts5.isIdentifier(call.expression)) {
|
|
945
|
+
const localDirect = localDirectHelpers.get(call.expression.text);
|
|
946
|
+
const resolved2 = localDirect ? { helper: localDirect, imp: void 0 } : await importedHelper(call.expression.text);
|
|
947
|
+
if (resolved2)
|
|
948
|
+
out.push({
|
|
949
|
+
variableName: targetName,
|
|
950
|
+
alias: resolved2.helper.alias,
|
|
951
|
+
aliasExpr: resolved2.helper.aliasExpr,
|
|
952
|
+
destinationExpr: resolved2.helper.destinationExpr,
|
|
953
|
+
servicePathExpr: resolved2.helper.servicePathExpr,
|
|
954
|
+
isDynamic: resolved2.helper.isDynamic,
|
|
955
|
+
placeholders: resolved2.helper.placeholders,
|
|
956
|
+
sourceFile: normalizePath(filePath),
|
|
957
|
+
sourceLine: lineOf3(sourceFileAst, node),
|
|
958
|
+
helperChain: [
|
|
959
|
+
...resolved2.helper.helperChain ?? [],
|
|
960
|
+
{
|
|
961
|
+
callerVariable: targetName,
|
|
962
|
+
...aliasKind === "assignment" ? { assignedFrom: call.expression.text, aliasKind, scopeRule: "same-file-source-order" } : {},
|
|
963
|
+
importedHelper: call.expression.text,
|
|
964
|
+
importSource: resolved2.imp?.sourceFile,
|
|
965
|
+
exportedSymbol: resolved2.imp?.exportedName ?? resolved2.helper.exportedName,
|
|
966
|
+
helperSourceFile: resolved2.helper.sourceFile,
|
|
967
|
+
helperSourceLine: resolved2.helper.sourceLine
|
|
968
|
+
}
|
|
969
|
+
]
|
|
970
|
+
});
|
|
1117
971
|
}
|
|
1118
972
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
const method = config && ts4.isObjectLiteralExpression(config) ? httpMethodFromObject(config, node) : void 0;
|
|
1123
|
-
const url = config && ts4.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, "url"), node) : { kind: "unknown", dynamic: false };
|
|
1124
|
-
return { method, externalTarget: destination ? { ...url, destination } : url, classifier: "sap_execute_http_request", sourceCallShape: "executeHttpRequest" };
|
|
973
|
+
async function recordVariable(decl) {
|
|
974
|
+
if (!ts5.isIdentifier(decl.name) || !decl.initializer) return;
|
|
975
|
+
await recordBindingFromExpression(decl.name.text, decl.initializer, decl, "declaration");
|
|
1125
976
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
}
|
|
1132
|
-
return { externalTarget: { kind: "unknown", dynamic: false }, classifier: "axios_unknown_call", sourceCallShape: "axios(...)" };
|
|
977
|
+
async function helpersForCall(call) {
|
|
978
|
+
if (!ts5.isIdentifier(call.expression)) return [];
|
|
979
|
+
const local = localObjectHelpers.get(call.expression.text) ?? [];
|
|
980
|
+
const imported = await importedHelpers(call.expression.text);
|
|
981
|
+
return [...local.map((helper) => ({ helper })), ...imported];
|
|
1133
982
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
const
|
|
1137
|
-
|
|
983
|
+
async function rememberObjectHelperVariable(decl) {
|
|
984
|
+
if (!ts5.isIdentifier(decl.name) || !decl.initializer) return;
|
|
985
|
+
const call = unwrapCall(decl.initializer);
|
|
986
|
+
if (!call) return;
|
|
987
|
+
const helpers = (await helpersForCall(call)).filter((row) => row.helper.returnedProperty);
|
|
988
|
+
if (helpers.length > 0) objectHelperVariables.set(decl.name.text, helpers);
|
|
1138
989
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
990
|
+
function recordObjectPropertyBinding(targetName, expr, node) {
|
|
991
|
+
const unwrapped = unwrapIdentityExpression(expr);
|
|
992
|
+
if (!ts5.isPropertyAccessExpression(unwrapped) || !ts5.isIdentifier(unwrapped.expression)) return false;
|
|
993
|
+
const helpers = objectHelperVariables.get(unwrapped.expression.text) ?? [];
|
|
994
|
+
const matches2 = helpers.filter((row) => row.helper.returnedProperty === unwrapped.name.text);
|
|
995
|
+
if (matches2.length !== 1) return false;
|
|
996
|
+
const resolved2 = matches2[0];
|
|
997
|
+
out.push({
|
|
998
|
+
variableName: targetName,
|
|
999
|
+
alias: resolved2.helper.alias,
|
|
1000
|
+
aliasExpr: resolved2.helper.aliasExpr,
|
|
1001
|
+
destinationExpr: resolved2.helper.destinationExpr,
|
|
1002
|
+
servicePathExpr: resolved2.helper.servicePathExpr,
|
|
1003
|
+
isDynamic: resolved2.helper.isDynamic,
|
|
1004
|
+
placeholders: resolved2.helper.placeholders,
|
|
1005
|
+
sourceFile: normalizePath(filePath),
|
|
1006
|
+
sourceLine: lineOf3(sourceFileAst, node),
|
|
1007
|
+
helperChain: [
|
|
1008
|
+
...resolved2.helper.helperChain ?? [],
|
|
1009
|
+
{
|
|
1010
|
+
callerVariable: targetName,
|
|
1011
|
+
sourceVariable: unwrapped.expression.text,
|
|
1012
|
+
returnedProperty: unwrapped.name.text,
|
|
1013
|
+
assignedFromProperty: unwrapped.getText(sourceFileAst),
|
|
1014
|
+
importSource: resolved2.imp?.sourceFile,
|
|
1015
|
+
exportedSymbol: resolved2.imp?.exportedName,
|
|
1016
|
+
helperSourceFile: resolved2.helper.sourceFile,
|
|
1017
|
+
helperSourceLine: resolved2.helper.sourceLine
|
|
1018
|
+
}
|
|
1019
|
+
]
|
|
1020
|
+
});
|
|
1021
|
+
return true;
|
|
1141
1022
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
if (
|
|
1148
|
-
|
|
1149
|
-
if (
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
if (ts4.isPropertyAccessExpression(expr)) return rootReceiverName(expr.expression);
|
|
1167
|
-
if (ts4.isCallExpression(expr)) return rootReceiverName(expr.expression);
|
|
1168
|
-
return void 0;
|
|
1169
|
-
}
|
|
1170
|
-
function isSupportedEventReceiver(receiver, rootReceiver, serviceVariables) {
|
|
1171
|
-
const candidate = rootReceiver ?? receiver;
|
|
1172
|
-
if (!candidate) return false;
|
|
1173
|
-
if (candidate === "cds") return true;
|
|
1174
|
-
if (serviceVariables.has(candidate)) return true;
|
|
1175
|
-
if (receiver && serviceVariables.has(receiver)) return true;
|
|
1176
|
-
if (/^(srv|service|serviceClient|messaging|messageClient|eventClient)$/.test(candidate)) return true;
|
|
1177
|
-
return false;
|
|
1178
|
-
}
|
|
1179
|
-
function collectWrapperSpecs(source) {
|
|
1180
|
-
const specs = /* @__PURE__ */ new Map();
|
|
1181
|
-
const serviceVariables = collectServiceVariables(source);
|
|
1182
|
-
const calledNames = /* @__PURE__ */ new Set();
|
|
1183
|
-
const collectCalls = (node) => {
|
|
1184
|
-
if (ts4.isCallExpression(node)) {
|
|
1185
|
-
if (ts4.isIdentifier(node.expression)) calledNames.add(node.expression.text);
|
|
1186
|
-
if (ts4.isCallExpression(node.expression) && ts4.isIdentifier(node.expression.expression)) calledNames.add(node.expression.expression.text);
|
|
1023
|
+
async function recordDestructuredHelper(decl) {
|
|
1024
|
+
if (!ts5.isObjectBindingPattern(decl.name) || !decl.initializer) return;
|
|
1025
|
+
const call = unwrapCall(decl.initializer);
|
|
1026
|
+
if (!call) return;
|
|
1027
|
+
const helpers = await helpersForCall(call);
|
|
1028
|
+
if (helpers.length === 0) return;
|
|
1029
|
+
for (const el of decl.name.elements) {
|
|
1030
|
+
if (!ts5.isIdentifier(el.name)) continue;
|
|
1031
|
+
const propertyName2 = el.propertyName && ts5.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
|
|
1032
|
+
const matches2 = helpers.filter((row) => row.helper.returnedProperty === propertyName2);
|
|
1033
|
+
if (matches2.length !== 1) continue;
|
|
1034
|
+
const resolved2 = matches2[0];
|
|
1035
|
+
out.push({
|
|
1036
|
+
variableName: el.name.text,
|
|
1037
|
+
alias: resolved2.helper.alias,
|
|
1038
|
+
aliasExpr: resolved2.helper.aliasExpr,
|
|
1039
|
+
destinationExpr: resolved2.helper.destinationExpr,
|
|
1040
|
+
servicePathExpr: resolved2.helper.servicePathExpr,
|
|
1041
|
+
isDynamic: resolved2.helper.isDynamic,
|
|
1042
|
+
placeholders: resolved2.helper.placeholders,
|
|
1043
|
+
sourceFile: normalizePath(filePath),
|
|
1044
|
+
sourceLine: lineOf3(sourceFileAst, decl),
|
|
1045
|
+
helperChain: [...resolved2.helper.helperChain ?? [], { callerVariable: el.name.text, helperFunction: call.expression.getText(sourceFileAst), returnedProperty: propertyName2, importSource: resolved2.imp?.sourceFile, exportedSymbol: resolved2.imp?.exportedName, helperSourceFile: resolved2.helper.sourceFile, helperSourceLine: resolved2.helper.sourceLine }]
|
|
1046
|
+
});
|
|
1187
1047
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
const methodLiteral = resolveExpression(methodProp, node, "literal").value;
|
|
1204
|
-
if (pathName) sends.push({ client: node.expression.expression.text, path: pathName, method: methodName, methodLiteral, start: node.getStart(source), end: node.getEnd() });
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
ts4.forEachChild(node, visit);
|
|
1208
|
-
};
|
|
1209
|
-
visit(fn);
|
|
1210
|
-
if (sends.length !== 1) return;
|
|
1211
|
-
const found = sends[0];
|
|
1212
|
-
const clientIndex = params.indexOf(found.client);
|
|
1213
|
-
const pathIndex = params.indexOf(found.path);
|
|
1214
|
-
const methodIndex = found.method ? params.indexOf(found.method) : -1;
|
|
1215
|
-
const capturesKnownClient = serviceVariables.has(found.client) || /^(srv|service|serviceClient|client|.*Client)$/.test(found.client);
|
|
1216
|
-
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, definitionLine: lineOf3(source.text, fn.getStart(source)), internalStart: found.start, internalEnd: found.end });
|
|
1217
|
-
};
|
|
1218
|
-
const visitTop = (node) => {
|
|
1219
|
-
if (ts4.isFunctionDeclaration(node) && node.name) scanFunction(node.name.text, node);
|
|
1220
|
-
if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.initializer && (ts4.isArrowFunction(node.initializer) || ts4.isFunctionExpression(node.initializer))) scanFunction(node.name.text, node.initializer);
|
|
1221
|
-
ts4.forEachChild(node, visitTop);
|
|
1222
|
-
};
|
|
1223
|
-
visitTop(source);
|
|
1224
|
-
return specs;
|
|
1225
|
-
}
|
|
1226
|
-
function classifyOutboundCallsInSource(source, filePath) {
|
|
1227
|
-
const calls = [];
|
|
1228
|
-
const sourceFile = normalizePath(filePath);
|
|
1229
|
-
const initializers = variableInitializers(source);
|
|
1230
|
-
const serviceVariables = collectServiceVariables(source);
|
|
1231
|
-
const wrapperSpecs = collectWrapperSpecs(source);
|
|
1232
|
-
const wrapperInternalRanges = [...wrapperSpecs.values()].map((spec) => ({ start: spec.internalStart, end: spec.internalEnd }));
|
|
1233
|
-
const add = (node, fact, extra) => {
|
|
1234
|
-
calls.push({ node, fact: { ...fact, sourceFile, sourceLine: lineOf3(source.text, node.getStart(source)), confidence: fact.confidence ?? 0.8, evidence: parserEvidence(source, node, extra) } });
|
|
1235
|
-
};
|
|
1236
|
-
const visit = (node) => {
|
|
1237
|
-
if (ts4.isCallExpression(node)) {
|
|
1238
|
-
if (wrapperInternalRanges.some((range) => node.getStart(source) >= range.start && node.getEnd() <= range.end)) {
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
1241
|
-
const expr = node.expression;
|
|
1242
|
-
const exprText = expr.getText(source);
|
|
1243
|
-
if (exprText === "cds.run") {
|
|
1244
|
-
const arg = node.arguments[0];
|
|
1245
|
-
const entity = arg ? queryEntityFromAst(arg, initializers) : void 0;
|
|
1246
|
-
const payload = arg?.getText(source) ?? "";
|
|
1247
|
-
add(node, { callType: "local_db_query", queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? void 0 : queryWarning(payload) });
|
|
1248
|
-
} else if (ts4.isPropertyAccessExpression(expr) && expr.name.text === "send" && (ts4.isIdentifier(expr.expression) || ts4.isPropertyAccessExpression(expr.expression))) {
|
|
1249
|
-
const objectArg = node.arguments[0];
|
|
1250
|
-
if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
|
|
1251
|
-
const receiver = receiverName(expr.expression);
|
|
1252
|
-
const query = objectPropertyText(objectArg, "query");
|
|
1253
|
-
const method = stripQuotes(resolveExpression(propertyInitializer(objectArg, "method"), node, "literal").value ?? objectPropertyText(objectArg, "method") ?? "POST");
|
|
1254
|
-
const pathExpr = propertyInitializer(objectArg, "path") ?? propertyInitializer(objectArg, "event");
|
|
1255
|
-
const resolvedPath = staticPathExpression(pathExpr, node);
|
|
1256
|
-
const op = pathExpr ? resolvedPath.text ?? pathExpr.getText(source) : void 0;
|
|
1257
|
-
const shorthandPath = objectPropertyIsShorthand(objectArg, "path");
|
|
1258
|
-
const candidateEvidence2 = pathExpr && ts4.isIdentifier(pathExpr) && !resolvedPath.text ? staticPathCandidates(pathExpr, node, method) : void 0;
|
|
1259
|
-
const candidateOperationPath = candidateEvidence2 && !candidateEvidence2.hasDynamicAssignments && candidateEvidence2.normalizedCandidateOperations.length === 1 && candidateEvidence2.candidatePaths.length > 0 ? candidateEvidence2.candidatePaths[0] : void 0;
|
|
1260
|
-
const operationPathExpr = operationPathFromStatic(resolvedPath.text) ?? candidateOperationPath;
|
|
1261
|
-
const intent = classifyODataPathIntent(operationPathExpr, method);
|
|
1262
|
-
const entityCallTypes = { entity_mutation: "remote_entity_mutation", entity_delete: "remote_entity_delete", entity_media: "remote_entity_media", entity_candidate: "remote_entity_candidate" };
|
|
1263
|
-
const entityCallType = entityCallTypes[intent.kind];
|
|
1264
|
-
const isODataQueryRead = method.toUpperCase() === "GET" && ["entity_query", "entity_key_read", "entity_navigation_query"].includes(intent.kind);
|
|
1265
|
-
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: !query && pathExpr && !operationPathExpr ? "dynamic_operation_path_identifier" : void 0 }, { receiver, classifier: "service_client_send_object", operationPathExpression: shorthandPath ? op : void 0, rawPathExpression: resolvedPath.rawExpression, literalPathSource: resolvedPath.text ? resolvedPath.sourceKind === "const" ? "same_scope_const_initializer" : resolvedPath.sourceKind : void 0, odataPathIntent: operationPathExpr ? intent : void 0, staticPathCandidates: candidateEvidence2, parserWarning: !query && pathExpr && !operationPathExpr ? "dynamic_operation_path_identifier" : void 0 });
|
|
1266
|
-
} else {
|
|
1267
|
-
const receiver = receiverName(expr.expression);
|
|
1268
|
-
const rootReceiver = rootReceiverName(expr.expression);
|
|
1269
|
-
const firstArg2 = resolveExpression(node.arguments[0], node, "literal");
|
|
1270
|
-
const method = firstArg2.value?.toUpperCase();
|
|
1271
|
-
const pathArg = node.arguments[1];
|
|
1272
|
-
const supported = method && supportedHttpMethods.has(method);
|
|
1273
|
-
if (receiver && supported && serviceVariables.has(rootReceiver ?? receiver)) {
|
|
1274
|
-
const resolvedPath = staticPathExpression(pathArg, node);
|
|
1275
|
-
const operationPathExpr = operationPathFromStatic(resolvedPath.text);
|
|
1276
|
-
const intent = classifyODataPathIntent(operationPathExpr, method);
|
|
1277
|
-
add(node, { callType: "remote_action", serviceVariableName: rootReceiver ?? receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: operationPathExpr ? 0.8 : 0.45, unresolvedReason: operationPathExpr ? void 0 : "dynamic_operation_path_identifier" }, { receiver, rootReceiver, classifier: "service_client_send_method_path", rawPathExpression: resolvedPath.rawExpression, literalPathSource: resolvedPath.text ? resolvedPath.sourceKind === "const" ? "same_scope_const_initializer" : resolvedPath.sourceKind : void 0, odataPathIntent: operationPathExpr ? intent : void 0, parserWarning: operationPathExpr ? void 0 : "dynamic_operation_path_identifier" });
|
|
1278
|
-
} else if (receiver && serviceVariables.has(rootReceiver ?? receiver)) {
|
|
1279
|
-
const operationPathExpr = safeOperationName(firstArg2.value);
|
|
1280
|
-
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" });
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
} else if (ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text) || ts4.isIdentifier(expr) && wrapperSpecs.has(expr.text)) {
|
|
1284
|
-
const wrapperName = ts4.isIdentifier(expr) ? expr.text : ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) ? expr.expression.text : "";
|
|
1285
|
-
const wrapperArgs = ts4.isIdentifier(expr) ? node.arguments : ts4.isCallExpression(expr) ? expr.arguments : node.arguments;
|
|
1286
|
-
const spec = wrapperSpecs.get(wrapperName);
|
|
1287
|
-
const clientArg = spec?.clientIndex === void 0 ? void 0 : wrapperArgs[spec.clientIndex];
|
|
1288
|
-
const pathArg = spec ? wrapperArgs[spec.pathIndex] : void 0;
|
|
1289
|
-
const methodArg = spec?.methodIndex === void 0 ? void 0 : wrapperArgs[spec.methodIndex];
|
|
1290
|
-
const receiver = clientArg && ts4.isIdentifier(clientArg) ? clientArg.text : spec?.clientName;
|
|
1291
|
-
const method = stripQuotes(resolveExpression(methodArg, node, "literal").value ?? spec?.methodLiteral ?? "POST");
|
|
1292
|
-
const resolvedPath = staticPathExpression(pathArg, node);
|
|
1293
|
-
const operationPathExpr = operationPathFromStatic(resolvedPath.text);
|
|
1294
|
-
const normalizedOperationPath = operationPathExpr ? classifyODataPathIntent(operationPathExpr, method).topLevelOperationName : void 0;
|
|
1295
|
-
if (spec && receiver && operationPathExpr) {
|
|
1296
|
-
add(node, { callType: "remote_action", serviceVariableName: receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.75 }, { receiver, classifier: resolvedPath.sourceKind === "literal" ? "higher_order_wrapper_literal_path" : "higher_order_wrapper_static_path", wrapperFunction: wrapperName, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf3(source.text, node.getStart(source)), wrapperPathSourceKind: resolvedPath.sourceKind, rawPathExpression: resolvedPath.rawExpression, normalizedOperationPath, literalPathSource: resolvedPath.sourceKind === "const" ? "same_scope_const_initializer" : `wrapper_call_${resolvedPath.sourceKind}`, literalCallerArgumentDetected: true });
|
|
1297
|
-
} else if (spec && receiver) {
|
|
1298
|
-
add(node, { callType: "remote_action", serviceVariableName: receiver, method, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.45, unresolvedReason: "dynamic_operation_path_identifier" }, { receiver, classifier: "higher_order_wrapper_dynamic_path", wrapperFunction: wrapperName, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf3(source.text, node.getStart(source)), wrapperPathSourceKind: resolvedPath.sourceKind, rawPathExpression: resolvedPath.rawExpression, parserWarning: "dynamic_operation_path_identifier" });
|
|
1299
|
-
}
|
|
1300
|
-
} else if (ts4.isPropertyAccessExpression(expr) && ["emit", "publish", "on"].includes(expr.name.text)) {
|
|
1301
|
-
const receiver = receiverName(expr.expression);
|
|
1302
|
-
const rootReceiver = rootReceiverName(expr.expression);
|
|
1303
|
-
if (isSupportedEventReceiver(receiver, rootReceiver, serviceVariables)) {
|
|
1304
|
-
const eventName = literalText(node.arguments[0]);
|
|
1305
|
-
if (eventName) add(node, { callType: expr.name.text === "on" ? "async_subscribe" : "async_emit", serviceVariableName: rootReceiver ?? receiver, eventNameExpr: eventName }, { receiver, rootReceiver, classifier: expr.name.text === "on" ? "cap_service_event_subscription" : "cap_service_event_emit", receiverClassification: "cap_evidence" });
|
|
1306
|
-
}
|
|
1307
|
-
} else {
|
|
1308
|
-
const external = externalHttpEvidence(node, source);
|
|
1309
|
-
if (external) {
|
|
1310
|
-
const evidenceTarget = { ...external.externalTarget, method: external.method, parserClassifier: external.classifier, sourceCallShape: external.sourceCallShape };
|
|
1311
|
-
const safeTarget = externalHttpTarget({ method: external.method, evidence_json: JSON.stringify({ externalTarget: evidenceTarget }) });
|
|
1312
|
-
add(node, { callType: "external_http", method: external.method, payloadSummary: void 0, confidence: 0.7, unresolvedReason: "External HTTP destination is outside indexed CAP services", externalTarget: { kind: safeTarget.kind, stableId: safeTarget.toId, label: safeTarget.label, dynamic: safeTarget.dynamic } }, { classifier: external.classifier, externalTarget: safeTarget, sourceCallShape: external.sourceCallShape });
|
|
1313
|
-
}
|
|
1048
|
+
}
|
|
1049
|
+
async function recordDestructuredAssignment(pattern, expr, node) {
|
|
1050
|
+
const call = unwrapCall(expr);
|
|
1051
|
+
if (!call) return;
|
|
1052
|
+
const helpers = await helpersForCall(call);
|
|
1053
|
+
if (helpers.length === 0) return;
|
|
1054
|
+
for (const prop of pattern.properties) {
|
|
1055
|
+
let propertyName2;
|
|
1056
|
+
let targetName;
|
|
1057
|
+
if (ts5.isShorthandPropertyAssignment(prop)) {
|
|
1058
|
+
propertyName2 = prop.name.text;
|
|
1059
|
+
targetName = prop.name.text;
|
|
1060
|
+
} else if (ts5.isPropertyAssignment(prop)) {
|
|
1061
|
+
propertyName2 = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
1062
|
+
targetName = ts5.isIdentifier(prop.initializer) ? prop.initializer.text : void 0;
|
|
1314
1063
|
}
|
|
1064
|
+
if (!propertyName2 || !targetName) continue;
|
|
1065
|
+
const matches2 = helpers.filter((row) => row.helper.returnedProperty === propertyName2);
|
|
1066
|
+
if (matches2.length !== 1) continue;
|
|
1067
|
+
const resolved2 = matches2[0];
|
|
1068
|
+
out.push({
|
|
1069
|
+
variableName: targetName,
|
|
1070
|
+
alias: resolved2.helper.alias,
|
|
1071
|
+
aliasExpr: resolved2.helper.aliasExpr,
|
|
1072
|
+
destinationExpr: resolved2.helper.destinationExpr,
|
|
1073
|
+
servicePathExpr: resolved2.helper.servicePathExpr,
|
|
1074
|
+
isDynamic: resolved2.helper.isDynamic,
|
|
1075
|
+
placeholders: resolved2.helper.placeholders,
|
|
1076
|
+
sourceFile: normalizePath(filePath),
|
|
1077
|
+
sourceLine: lineOf3(sourceFileAst, node),
|
|
1078
|
+
helperChain: [...resolved2.helper.helperChain ?? [], { callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind: "assignment", scopeRule: "same-file-source-order", returnedProperty: propertyName2, importSource: resolved2.imp?.sourceFile, exportedSymbol: resolved2.imp?.exportedName, helperSourceFile: resolved2.helper.sourceFile, helperSourceLine: resolved2.helper.sourceLine }]
|
|
1079
|
+
});
|
|
1315
1080
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
const source = ts4.createSourceFile(filePath, text, ts4.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts4.ScriptKind.TS : ts4.ScriptKind.JS);
|
|
1334
|
-
const aliases = /* @__PURE__ */ new Map();
|
|
1335
|
-
const calls = [];
|
|
1336
|
-
const visit = (node) => {
|
|
1337
|
-
if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.initializer) {
|
|
1338
|
-
const origin = serviceLookup(node.initializer, aliases);
|
|
1339
|
-
if (origin) aliases.set(node.name.text, { ...origin, chain: [...origin.chain, node.name.text] });
|
|
1340
|
-
}
|
|
1341
|
-
if (ts4.isCallExpression(node)) {
|
|
1342
|
-
const parsed = serviceOperationCall(node, aliases);
|
|
1343
|
-
if (parsed && parsed.operation !== "entities") calls.push({
|
|
1344
|
-
callType: "local_service_call",
|
|
1345
|
-
operationPathExpr: `/${parsed.operation}`,
|
|
1346
|
-
payloadSummary: parsed.service,
|
|
1347
|
-
localServiceName: parsed.service,
|
|
1348
|
-
localServiceLookup: parsed.lookup,
|
|
1349
|
-
aliasChain: parsed.chain,
|
|
1081
|
+
}
|
|
1082
|
+
function recordDestructuredClassHelper(decl) {
|
|
1083
|
+
if (!ts5.isObjectBindingPattern(decl.name) || !decl.initializer) return;
|
|
1084
|
+
const call = unwrapCall(decl.initializer);
|
|
1085
|
+
if (!call || !ts5.isPropertyAccessExpression(call.expression)) return;
|
|
1086
|
+
const target = call.expression;
|
|
1087
|
+
if (target.expression.kind !== ts5.SyntaxKind.ThisKeyword) return;
|
|
1088
|
+
for (const el of decl.name.elements) {
|
|
1089
|
+
if (!ts5.isIdentifier(el.name)) continue;
|
|
1090
|
+
const propertyName2 = el.propertyName && ts5.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
|
|
1091
|
+
const helper = classHelpers.find(
|
|
1092
|
+
(h) => h.helperName === target.name.text && h.propertyName === propertyName2
|
|
1093
|
+
);
|
|
1094
|
+
if (!helper) continue;
|
|
1095
|
+
out.push({
|
|
1096
|
+
variableName: el.name.text,
|
|
1097
|
+
...helper.fact,
|
|
1350
1098
|
sourceFile: normalizePath(filePath),
|
|
1351
|
-
sourceLine: lineOf3(
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1099
|
+
sourceLine: lineOf3(sourceFileAst, decl),
|
|
1100
|
+
helperChain: [
|
|
1101
|
+
{
|
|
1102
|
+
callerVariable: el.name.text,
|
|
1103
|
+
className: helper.className,
|
|
1104
|
+
classHelper: helper.helperName,
|
|
1105
|
+
returnedProperty: helper.propertyName,
|
|
1106
|
+
helperVariable: helper.variableName,
|
|
1107
|
+
helperSourceFile: normalizePath(filePath),
|
|
1108
|
+
helperSourceLine: helper.sourceLine
|
|
1109
|
+
}
|
|
1110
|
+
]
|
|
1362
1111
|
});
|
|
1363
1112
|
}
|
|
1364
|
-
ts4.forEachChild(node, visit);
|
|
1365
|
-
};
|
|
1366
|
-
visit(source);
|
|
1367
|
-
return calls;
|
|
1368
|
-
}
|
|
1369
|
-
function serviceLookup(expr, aliases) {
|
|
1370
|
-
if (ts4.isIdentifier(expr)) return aliases.get(expr.text);
|
|
1371
|
-
if (ts4.isPropertyAccessExpression(expr) && expr.expression.getText() === "cds.services") return { service: expr.name.text, lookup: expr.getText(), chain: [expr.getText()] };
|
|
1372
|
-
if (ts4.isElementAccessExpression(expr) && expr.expression.getText() === "cds.services" && ts4.isStringLiteral(expr.argumentExpression)) return { service: expr.argumentExpression.text, lookup: expr.getText(), chain: [expr.getText()] };
|
|
1373
|
-
return void 0;
|
|
1374
|
-
}
|
|
1375
|
-
function serviceOperationCall(node, aliases) {
|
|
1376
|
-
const expr = node.expression;
|
|
1377
|
-
if (!ts4.isPropertyAccessExpression(expr)) return void 0;
|
|
1378
|
-
const origin = serviceLookup(expr.expression, aliases);
|
|
1379
|
-
if (!origin) return void 0;
|
|
1380
|
-
if (expr.name.text === "send") {
|
|
1381
|
-
const first = literalText(node.arguments[0]);
|
|
1382
|
-
const second = literalText(node.arguments[1]);
|
|
1383
|
-
const method = first?.toUpperCase();
|
|
1384
|
-
if (method && ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"].includes(method) && second) return { ...origin, operation: second.replace(/^\//, ""), classifier: "cap_service_send_method_path" };
|
|
1385
|
-
if (first) return { ...origin, operation: first.replace(/^\//, ""), classifier: "cap_service_send_local_dispatch" };
|
|
1386
1113
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
if (!node) return void 0;
|
|
1399
|
-
if (ts5.isStringLiteralLike(node) || ts5.isNoSubstitutionTemplateLiteral(node))
|
|
1400
|
-
return node.text;
|
|
1401
|
-
if (ts5.isTemplateExpression(node))
|
|
1402
|
-
return node.getText().replace(/^`|`$/g, "");
|
|
1403
|
-
return node.getText();
|
|
1404
|
-
}
|
|
1405
|
-
function placeholders2(value) {
|
|
1406
|
-
return [...(value ?? "").matchAll(/\$\{([^}]*)\}/g)].map((m) => (m[1] ?? "").trim()).filter(Boolean);
|
|
1407
|
-
}
|
|
1408
|
-
function connectFactFromCall(call) {
|
|
1409
|
-
const expr = call.expression;
|
|
1410
|
-
if (!ts5.isPropertyAccessExpression(expr) || expr.name.text !== "to")
|
|
1411
|
-
return void 0;
|
|
1412
|
-
const inner = expr.expression;
|
|
1413
|
-
if (!ts5.isPropertyAccessExpression(inner) || inner.name.text !== "connect" || inner.expression.getText() !== "cds")
|
|
1414
|
-
return void 0;
|
|
1415
|
-
const first = call.arguments[0];
|
|
1416
|
-
if (!first) return void 0;
|
|
1417
|
-
const second = call.arguments[1];
|
|
1418
|
-
const objectArg = ts5.isObjectLiteralExpression(first) ? first : second && ts5.isObjectLiteralExpression(second) ? second : void 0;
|
|
1419
|
-
let alias;
|
|
1420
|
-
let aliasExpr;
|
|
1421
|
-
if (ts5.isStringLiteralLike(first) || ts5.isNoSubstitutionTemplateLiteral(first))
|
|
1422
|
-
alias = first.text;
|
|
1423
|
-
else if (!ts5.isObjectLiteralExpression(first))
|
|
1424
|
-
aliasExpr = stringValue(first);
|
|
1425
|
-
if ((ts5.isStringLiteralLike(first) || ts5.isNoSubstitutionTemplateLiteral(first)) && !objectArg)
|
|
1426
|
-
return { alias: first.text, isDynamic: false, placeholders: [] };
|
|
1427
|
-
if (!objectArg && aliasExpr)
|
|
1428
|
-
return {
|
|
1429
|
-
aliasExpr,
|
|
1430
|
-
isDynamic: true,
|
|
1431
|
-
placeholders: placeholders2(aliasExpr)
|
|
1432
|
-
};
|
|
1433
|
-
let destinationExpr;
|
|
1434
|
-
let servicePathExpr;
|
|
1435
|
-
function visitObject(obj) {
|
|
1436
|
-
for (const prop of obj.properties) {
|
|
1437
|
-
if (!ts5.isPropertyAssignment(prop)) continue;
|
|
1438
|
-
const name = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
1439
|
-
if (name === "destination")
|
|
1440
|
-
destinationExpr = stringValue(prop.initializer);
|
|
1441
|
-
if (name === "path" || name === "servicePath")
|
|
1442
|
-
servicePathExpr = stringValue(prop.initializer);
|
|
1443
|
-
if (ts5.isObjectLiteralExpression(prop.initializer))
|
|
1444
|
-
visitObject(prop.initializer);
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
if (objectArg) visitObject(objectArg);
|
|
1448
|
-
const ph = [
|
|
1449
|
-
...placeholders2(aliasExpr ?? alias),
|
|
1450
|
-
...placeholders2(destinationExpr),
|
|
1451
|
-
...placeholders2(servicePathExpr)
|
|
1452
|
-
];
|
|
1453
|
-
return {
|
|
1454
|
-
alias,
|
|
1455
|
-
aliasExpr,
|
|
1456
|
-
destinationExpr,
|
|
1457
|
-
servicePathExpr,
|
|
1458
|
-
isDynamic: ph.length > 0 || !destinationExpr && !servicePathExpr,
|
|
1459
|
-
placeholders: ph
|
|
1460
|
-
};
|
|
1461
|
-
}
|
|
1462
|
-
function unwrapCall(expr) {
|
|
1463
|
-
if (ts5.isAwaitExpression(expr)) return unwrapCall(expr.expression);
|
|
1464
|
-
if (ts5.isParenthesizedExpression(expr)) return unwrapCall(expr.expression);
|
|
1465
|
-
if (ts5.isAsExpression(expr) || ts5.isSatisfiesExpression(expr)) return unwrapCall(expr.expression);
|
|
1466
|
-
if (ts5.isTypeAssertionExpression(expr)) return unwrapCall(expr.expression);
|
|
1467
|
-
if (ts5.isCallExpression(expr)) return expr;
|
|
1468
|
-
return void 0;
|
|
1469
|
-
}
|
|
1470
|
-
function unwrapIdentityExpression(expr) {
|
|
1471
|
-
if (ts5.isAwaitExpression(expr)) return unwrapIdentityExpression(expr.expression);
|
|
1472
|
-
if (ts5.isParenthesizedExpression(expr)) return unwrapIdentityExpression(expr.expression);
|
|
1473
|
-
if (ts5.isAsExpression(expr) || ts5.isSatisfiesExpression(expr)) return unwrapIdentityExpression(expr.expression);
|
|
1474
|
-
if (ts5.isTypeAssertionExpression(expr)) return unwrapIdentityExpression(expr.expression);
|
|
1475
|
-
return expr;
|
|
1476
|
-
}
|
|
1477
|
-
function transactionReceiverName(expr) {
|
|
1478
|
-
const call = unwrapCall(expr);
|
|
1479
|
-
if (call && ts5.isPropertyAccessExpression(call.expression) && ["tx", "transaction"].includes(call.expression.name.text) && ts5.isIdentifier(call.expression.expression)) return call.expression.expression.text;
|
|
1480
|
-
const unwrapped = unwrapIdentityExpression(expr);
|
|
1481
|
-
if (ts5.isConditionalExpression(unwrapped)) {
|
|
1482
|
-
const left = transactionReceiverName(unwrapped.whenTrue);
|
|
1483
|
-
const right = transactionReceiverName(unwrapped.whenFalse);
|
|
1484
|
-
return left && left === right ? left : void 0;
|
|
1485
|
-
}
|
|
1486
|
-
return void 0;
|
|
1487
|
-
}
|
|
1488
|
-
function findConnectInExpression(expr) {
|
|
1489
|
-
const direct = unwrapCall(expr);
|
|
1490
|
-
if (direct) {
|
|
1491
|
-
const fact = connectFactFromCall(direct);
|
|
1492
|
-
if (fact) return fact;
|
|
1114
|
+
function arrayElementsFromExpression(expr) {
|
|
1115
|
+
const unwrapped = unwrapIdentityExpression(expr);
|
|
1116
|
+
if (ts5.isArrayLiteralExpression(unwrapped)) return { elements: unwrapped.elements, promiseAll: false };
|
|
1117
|
+
const call = unwrapCall(expr);
|
|
1118
|
+
if (!call) return void 0;
|
|
1119
|
+
if (!ts5.isPropertyAccessExpression(call.expression) || call.expression.name.text !== "all" || call.expression.expression.getText(sourceFileAst) !== "Promise") return void 0;
|
|
1120
|
+
const first = call.arguments[0];
|
|
1121
|
+
if (!first) return void 0;
|
|
1122
|
+
const container = unwrapIdentityExpression(first);
|
|
1123
|
+
if (!ts5.isArrayLiteralExpression(container)) return void 0;
|
|
1124
|
+
return { elements: container.elements, promiseAll: true };
|
|
1493
1125
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
if (
|
|
1498
|
-
|
|
1126
|
+
async function recordArrayElementBinding(targetName, expr, node, arrayIndex, promiseAll) {
|
|
1127
|
+
const before = out.length;
|
|
1128
|
+
await recordBindingFromExpression(targetName, expr, node, "declaration");
|
|
1129
|
+
if (out.length > before) {
|
|
1130
|
+
const row = out[out.length - 1];
|
|
1131
|
+
row.helperChain = [
|
|
1132
|
+
...row.helperChain ?? [],
|
|
1133
|
+
{ callerVariable: targetName, targetVariable: targetName, arrayIndex, promiseAll, arrayContainer: promiseAll ? "Promise.all" : "array_literal" }
|
|
1134
|
+
];
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
const unwrapped = unwrapIdentityExpression(expr);
|
|
1138
|
+
if (ts5.isIdentifier(unwrapped)) {
|
|
1139
|
+
const existing = bindingForVariable(unwrapped.text);
|
|
1140
|
+
if (!existing) return;
|
|
1141
|
+
out.push({
|
|
1142
|
+
...existing,
|
|
1143
|
+
variableName: targetName,
|
|
1144
|
+
sourceLine: lineOf3(sourceFileAst, node),
|
|
1145
|
+
helperChain: [
|
|
1146
|
+
...existing.helperChain ?? [],
|
|
1147
|
+
{ callerVariable: targetName, targetVariable: targetName, sourceVariable: unwrapped.text, aliasKind: "array-destructuring", arrayIndex, promiseAll, arrayContainer: promiseAll ? "Promise.all" : "array_literal" }
|
|
1148
|
+
]
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1499
1151
|
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
ts5.
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
);
|
|
1513
|
-
} catch {
|
|
1514
|
-
return void 0;
|
|
1152
|
+
async function recordArrayDestructuredVariable(decl) {
|
|
1153
|
+
if (!ts5.isArrayBindingPattern(decl.name) || !decl.initializer) return;
|
|
1154
|
+
const container = arrayElementsFromExpression(decl.initializer);
|
|
1155
|
+
if (!container) return;
|
|
1156
|
+
for (let index = 0; index < decl.name.elements.length; index += 1) {
|
|
1157
|
+
const el = decl.name.elements[index];
|
|
1158
|
+
if (!el || ts5.isOmittedExpression(el) || ts5.isBindingElement(el) && el.dotDotDotToken) continue;
|
|
1159
|
+
if (!ts5.isBindingElement(el) || !ts5.isIdentifier(el.name)) continue;
|
|
1160
|
+
const source = container.elements[index];
|
|
1161
|
+
if (!source || ts5.isOmittedExpression(source)) continue;
|
|
1162
|
+
await recordArrayElementBinding(el.name.text, source, decl, index, container.promiseAll);
|
|
1163
|
+
}
|
|
1515
1164
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
`${base}.js`,
|
|
1526
|
-
path8.join(base, "index.ts"),
|
|
1527
|
-
path8.join(base, "index.js")
|
|
1528
|
-
]) {
|
|
1529
|
-
try {
|
|
1530
|
-
const st = await fs7.stat(candidate);
|
|
1531
|
-
if (st.isFile()) return normalizePath(path8.relative(repoPath, candidate));
|
|
1532
|
-
} catch {
|
|
1165
|
+
async function recordArrayDestructuredAssignment(pattern, expr, node) {
|
|
1166
|
+
const container = arrayElementsFromExpression(expr);
|
|
1167
|
+
if (!container) return;
|
|
1168
|
+
for (let index = 0; index < pattern.elements.length; index += 1) {
|
|
1169
|
+
const el = pattern.elements[index];
|
|
1170
|
+
if (!el || ts5.isOmittedExpression(el) || ts5.isSpreadElement(el) || !ts5.isIdentifier(el)) continue;
|
|
1171
|
+
const source = container.elements[index];
|
|
1172
|
+
if (!source || ts5.isOmittedExpression(source)) continue;
|
|
1173
|
+
await recordArrayElementBinding(el.text, source, node, index, container.promiseAll);
|
|
1533
1174
|
}
|
|
1534
1175
|
}
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
continue;
|
|
1542
|
-
const sourceFile = await resolveImport(
|
|
1543
|
-
repoPath,
|
|
1544
|
-
filePath,
|
|
1545
|
-
stmt.moduleSpecifier.text
|
|
1546
|
-
);
|
|
1547
|
-
const clause = stmt.importClause;
|
|
1548
|
-
if (!clause) continue;
|
|
1549
|
-
if (clause.name)
|
|
1550
|
-
imports.push({
|
|
1551
|
-
localName: clause.name.text,
|
|
1552
|
-
exportedName: "default",
|
|
1553
|
-
sourceFile
|
|
1554
|
-
});
|
|
1555
|
-
const bindings = clause.namedBindings;
|
|
1556
|
-
if (bindings && ts5.isNamedImports(bindings))
|
|
1557
|
-
for (const el of bindings.elements)
|
|
1558
|
-
imports.push({
|
|
1559
|
-
localName: el.name.text,
|
|
1560
|
-
exportedName: el.propertyName?.text ?? el.name.text,
|
|
1561
|
-
sourceFile
|
|
1562
|
-
});
|
|
1176
|
+
const events = [];
|
|
1177
|
+
function collectEvents(node) {
|
|
1178
|
+
if (ts5.isVariableDeclaration(node)) events.push({ pos: node.getStart(sourceFileAst), node });
|
|
1179
|
+
if (ts5.isBinaryExpression(node) && node.operatorToken.kind === ts5.SyntaxKind.EqualsToken)
|
|
1180
|
+
events.push({ pos: node.getStart(sourceFileAst), node });
|
|
1181
|
+
ts5.forEachChild(node, collectEvents);
|
|
1563
1182
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
if (
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1183
|
+
collectEvents(sourceFileAst);
|
|
1184
|
+
events.sort((a, b) => a.pos - b.pos);
|
|
1185
|
+
for (const event of events) {
|
|
1186
|
+
if (ts5.isVariableDeclaration(event.node)) {
|
|
1187
|
+
const decl = event.node;
|
|
1188
|
+
await recordDestructuredHelper(decl);
|
|
1189
|
+
await recordArrayDestructuredVariable(decl);
|
|
1190
|
+
recordDestructuredClassHelper(decl);
|
|
1191
|
+
await rememberObjectHelperVariable(decl);
|
|
1192
|
+
if (ts5.isIdentifier(decl.name) && decl.initializer) recordObjectPropertyBinding(decl.name.text, decl.initializer, decl);
|
|
1193
|
+
await recordVariable(decl);
|
|
1194
|
+
recordIdentityAlias(decl);
|
|
1195
|
+
if (ts5.isIdentifier(decl.name) && decl.initializer) {
|
|
1196
|
+
const sourceName = transactionReceiverName(decl.initializer);
|
|
1197
|
+
if (sourceName) cloneAliasBinding(decl.name.text, sourceName, "transaction", decl);
|
|
1578
1198
|
}
|
|
1199
|
+
continue;
|
|
1579
1200
|
}
|
|
1580
|
-
|
|
1201
|
+
const assignment = event.node;
|
|
1202
|
+
if (ts5.isIdentifier(assignment.left)) {
|
|
1203
|
+
const rhs = unwrapIdentityExpression(assignment.right);
|
|
1204
|
+
if (ts5.isIdentifier(rhs)) {
|
|
1205
|
+
cloneAliasBinding(assignment.left.text, rhs.text, "identity-assignment", assignment);
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
if (recordObjectPropertyBinding(assignment.left.text, assignment.right, assignment)) continue;
|
|
1209
|
+
await recordBindingFromExpression(assignment.left.text, assignment.right, assignment, "assignment");
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
const left = ts5.isParenthesizedExpression(assignment.left) ? assignment.left.expression : assignment.left;
|
|
1213
|
+
if (ts5.isObjectLiteralExpression(left))
|
|
1214
|
+
await recordDestructuredAssignment(left, assignment.right, assignment);
|
|
1215
|
+
if (ts5.isArrayLiteralExpression(left))
|
|
1216
|
+
await recordArrayDestructuredAssignment(left, assignment.right, assignment);
|
|
1581
1217
|
}
|
|
1582
|
-
|
|
1583
|
-
return bindings;
|
|
1218
|
+
return out;
|
|
1584
1219
|
}
|
|
1585
|
-
function
|
|
1586
|
-
const
|
|
1587
|
-
const
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.initializer)) {
|
|
1595
|
-
const propertyName = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
1596
|
-
if (propertyName) returns.set(propertyName, prop.initializer.text);
|
|
1220
|
+
function collectClassHelpers(sf) {
|
|
1221
|
+
const helpers = [];
|
|
1222
|
+
for (const stmt of sf.statements) {
|
|
1223
|
+
if (!ts5.isClassDeclaration(stmt) || !stmt.name) continue;
|
|
1224
|
+
for (const member of stmt.members) {
|
|
1225
|
+
let visit2 = function(node) {
|
|
1226
|
+
if (ts5.isVariableDeclaration(node) && ts5.isIdentifier(node.name) && node.initializer) {
|
|
1227
|
+
const fact = findConnectInExpression(node.initializer);
|
|
1228
|
+
if (fact) bindings.set(node.name.text, fact);
|
|
1597
1229
|
}
|
|
1598
|
-
|
|
1230
|
+
if (ts5.isReturnStatement(node) && node.expression && ts5.isObjectLiteralExpression(node.expression)) {
|
|
1231
|
+
for (const prop of node.expression.properties) {
|
|
1232
|
+
if (ts5.isShorthandPropertyAssignment(prop)) {
|
|
1233
|
+
const fact = bindings.get(prop.name.text);
|
|
1234
|
+
if (fact)
|
|
1235
|
+
helpers.push({
|
|
1236
|
+
className,
|
|
1237
|
+
helperName,
|
|
1238
|
+
propertyName: prop.name.text,
|
|
1239
|
+
variableName: prop.name.text,
|
|
1240
|
+
fact,
|
|
1241
|
+
sourceLine: lineOf3(sf, prop)
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.initializer)) {
|
|
1245
|
+
const propertyName2 = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
1246
|
+
const fact = propertyName2 ? bindings.get(prop.initializer.text) : void 0;
|
|
1247
|
+
if (propertyName2 && fact)
|
|
1248
|
+
helpers.push({
|
|
1249
|
+
className,
|
|
1250
|
+
helperName,
|
|
1251
|
+
propertyName: propertyName2,
|
|
1252
|
+
variableName: prop.initializer.text,
|
|
1253
|
+
fact,
|
|
1254
|
+
sourceLine: lineOf3(sf, prop)
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
ts5.forEachChild(node, visit2);
|
|
1260
|
+
};
|
|
1261
|
+
var visit = visit2;
|
|
1262
|
+
if (!ts5.isPropertyDeclaration(member) || !member.initializer) continue;
|
|
1263
|
+
if (!ts5.isIdentifier(member.name)) continue;
|
|
1264
|
+
const className = stmt.name.text;
|
|
1265
|
+
const helperName = member.name.text;
|
|
1266
|
+
const initializer = member.initializer;
|
|
1267
|
+
if (!ts5.isArrowFunction(initializer) && !ts5.isFunctionExpression(initializer))
|
|
1268
|
+
continue;
|
|
1269
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
1270
|
+
visit2(initializer);
|
|
1599
1271
|
}
|
|
1600
|
-
ts5.forEachChild(node, visit);
|
|
1601
|
-
}
|
|
1602
|
-
visit(fn);
|
|
1603
|
-
const out = /* @__PURE__ */ new Map();
|
|
1604
|
-
for (const [propertyName, variableName] of returns) {
|
|
1605
|
-
const fact = bindings.get(variableName);
|
|
1606
|
-
if (fact) out.set(propertyName, fact);
|
|
1607
1272
|
}
|
|
1608
|
-
return
|
|
1273
|
+
return helpers;
|
|
1609
1274
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1275
|
+
|
|
1276
|
+
// src/parsers/outbound-call-parser.ts
|
|
1277
|
+
import fs7 from "fs/promises";
|
|
1278
|
+
import path10 from "path";
|
|
1279
|
+
import ts7 from "typescript";
|
|
1280
|
+
|
|
1281
|
+
// src/linker/external-http-target.ts
|
|
1282
|
+
import { createHash } from "crypto";
|
|
1283
|
+
var sensitiveKeys = /* @__PURE__ */ new Set(["token", "access_token", "id_token", "api_key", "apikey", "key", "password", "passwd", "pwd", "secret", "client_secret", "authorization", "cookie", "signature"]);
|
|
1284
|
+
function hash(value) {
|
|
1285
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
1614
1286
|
}
|
|
1615
|
-
function
|
|
1616
|
-
|
|
1617
|
-
let returned;
|
|
1618
|
-
function visit(node) {
|
|
1619
|
-
if (node !== fn && (ts5.isFunctionDeclaration(node) || ts5.isArrowFunction(node) || ts5.isFunctionExpression(node)))
|
|
1620
|
-
return;
|
|
1621
|
-
if (!returned && ts5.isReturnStatement(node) && node.expression)
|
|
1622
|
-
returned = node.expression;
|
|
1623
|
-
if (!returned) ts5.forEachChild(node, visit);
|
|
1624
|
-
}
|
|
1625
|
-
visit(fn);
|
|
1626
|
-
if (!returned) return void 0;
|
|
1627
|
-
if (ts5.isIdentifier(returned)) return localBindings.get(returned.text);
|
|
1628
|
-
return findConnectInExpression(returned);
|
|
1629
|
-
}
|
|
1630
|
-
function directConnectFactFromFunctionLike(fn) {
|
|
1631
|
-
if (ts5.isArrowFunction(fn) && fn.body && !ts5.isBlock(fn.body))
|
|
1632
|
-
return findConnectInExpression(fn.body);
|
|
1633
|
-
return directReturnConnectFact(fn);
|
|
1287
|
+
function methodPrefix(method) {
|
|
1288
|
+
return typeof method === "string" && method.length > 0 ? `${method.toUpperCase()} ` : "";
|
|
1634
1289
|
}
|
|
1635
|
-
function
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
)
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
if (ts5.isIdentifier(decl.name)) exports.set(decl.name.text, decl.name.text);
|
|
1646
|
-
}
|
|
1647
|
-
if (!ts5.isExportDeclaration(stmt) || !stmt.exportClause) continue;
|
|
1648
|
-
if (!ts5.isNamedExports(stmt.exportClause)) continue;
|
|
1649
|
-
for (const el of stmt.exportClause.elements)
|
|
1650
|
-
exports.set(el.name.text, el.propertyName?.text ?? el.name.text);
|
|
1290
|
+
function redactUrl(value) {
|
|
1291
|
+
try {
|
|
1292
|
+
const url = new URL(value, value.startsWith("/") ? "https://relative.invalid" : void 0);
|
|
1293
|
+
url.username = "";
|
|
1294
|
+
url.password = "";
|
|
1295
|
+
for (const key of [...url.searchParams.keys()]) url.searchParams.set(key, sensitiveKeys.has(key.toLowerCase()) ? "<redacted>" : "<redacted>");
|
|
1296
|
+
const path11 = `${url.pathname}${url.search ? url.search : ""}`;
|
|
1297
|
+
return value.startsWith("/") ? path11 : `${url.origin}${path11}`;
|
|
1298
|
+
} catch {
|
|
1299
|
+
return value.replace(/([?&][^=;&]*(?:token|key|password|secret|cookie|authorization)[^=;&]*=)[^&]*/gi, "$1<redacted>");
|
|
1651
1300
|
}
|
|
1652
|
-
return exports;
|
|
1653
1301
|
}
|
|
1654
|
-
|
|
1655
|
-
const
|
|
1656
|
-
|
|
1657
|
-
const
|
|
1658
|
-
const
|
|
1659
|
-
const
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
if (fact) factsByLocal.set(stmt.name.text, { ...fact, sourceLine: lineOf4(sf, stmt) });
|
|
1665
|
-
for (const [returnedProperty, objectFact] of collectReturnedObjectBindings(stmt))
|
|
1666
|
-
factsByLocal.set(`${stmt.name.text}#${returnedProperty}`, { ...objectFact, returnedProperty, sourceLine: lineOf4(sf, stmt) });
|
|
1667
|
-
}
|
|
1668
|
-
if (ts5.isVariableStatement(stmt))
|
|
1669
|
-
for (const decl of stmt.declarationList.declarations) {
|
|
1670
|
-
if (!ts5.isIdentifier(decl.name) || !decl.initializer) continue;
|
|
1671
|
-
const helper = functionLikeInitializer(decl.initializer);
|
|
1672
|
-
if (helper) {
|
|
1673
|
-
const directReturn = directConnectFactFromFunctionLike(helper);
|
|
1674
|
-
if (directReturn)
|
|
1675
|
-
factsByLocal.set(decl.name.text, {
|
|
1676
|
-
...directReturn,
|
|
1677
|
-
sourceLine: lineOf4(sourceFileAst, decl)
|
|
1678
|
-
});
|
|
1679
|
-
for (const [returnedProperty, objectFact] of collectReturnedObjectBindings(helper))
|
|
1680
|
-
factsByLocal.set(`${decl.name.text}#${returnedProperty}`, {
|
|
1681
|
-
...objectFact,
|
|
1682
|
-
returnedProperty,
|
|
1683
|
-
sourceLine: lineOf4(sourceFileAst, decl)
|
|
1684
|
-
});
|
|
1685
|
-
continue;
|
|
1686
|
-
}
|
|
1687
|
-
const fact = findConnectInExpression(decl.initializer);
|
|
1688
|
-
if (fact)
|
|
1689
|
-
factsByLocal.set(decl.name.text, {
|
|
1690
|
-
...fact,
|
|
1691
|
-
sourceLine: lineOf4(sourceFileAst, decl)
|
|
1692
|
-
});
|
|
1693
|
-
}
|
|
1302
|
+
function externalHttpTarget(call) {
|
|
1303
|
+
const evidence = typeof call.evidence_json === "string" ? safeParse(call.evidence_json) : {};
|
|
1304
|
+
const target = evidence.externalTarget && typeof evidence.externalTarget === "object" && !Array.isArray(evidence.externalTarget) ? evidence.externalTarget : {};
|
|
1305
|
+
const method = typeof call.method === "string" ? call.method : typeof target.method === "string" ? target.method : void 0;
|
|
1306
|
+
const kind = typeof target.kind === "string" ? target.kind : "unknown";
|
|
1307
|
+
const expression = typeof target.expression === "string" ? target.expression : void 0;
|
|
1308
|
+
if (kind === "destination" && target.dynamic === true) {
|
|
1309
|
+
const shape = typeof target.expressionShape === "string" ? target.expressionShape : "expression";
|
|
1310
|
+
const candidates = Array.isArray(target.candidateLiterals) ? target.candidateLiterals.filter((item) => typeof item === "string") : [];
|
|
1311
|
+
return { kind, toKind: "external_destination", toId: `destination:dynamic:${hash(`${shape}:${candidates.join("|")}`)}`, label: "External destination: dynamic destination", method, dynamic: true, expression: candidates.length ? `candidates:${candidates.join("|")}` : `shape:${shape}` };
|
|
1694
1312
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
...fact,
|
|
1700
|
-
exportedName,
|
|
1701
|
-
sourceFile: normalizePath(filePath),
|
|
1702
|
-
sourceLine: fact.sourceLine
|
|
1703
|
-
});
|
|
1313
|
+
if (kind === "destination" && expression) return { kind, toKind: "external_destination", toId: `destination:${expression}`, label: `External destination: ${expression}`, method, dynamic: false, expression };
|
|
1314
|
+
if (kind === "static_url" && expression) {
|
|
1315
|
+
const redacted = redactUrl(expression);
|
|
1316
|
+
return { kind, toKind: "external_endpoint", toId: `endpoint:${hash(`${method ?? ""}:${redacted}`)}`, label: `External endpoint: ${methodPrefix(method)}${redacted}`, method, dynamic: false, expression: redacted };
|
|
1704
1317
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
}
|
|
1318
|
+
if (kind === "url_expression" && expression) return { kind, toKind: "external_endpoint", toId: `dynamic:${hash(expression)}`, label: `External endpoint: ${methodPrefix(method)}dynamic URL`, method, dynamic: true, expression: `expr:${hash(expression)}` };
|
|
1319
|
+
return { kind: "unknown", toKind: "external_endpoint", toId: "unknown", label: "External endpoint: unknown", method, dynamic: false };
|
|
1320
|
+
}
|
|
1321
|
+
function safeParse(value) {
|
|
1322
|
+
try {
|
|
1323
|
+
const parsed = JSON.parse(value);
|
|
1324
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1325
|
+
} catch {
|
|
1326
|
+
return {};
|
|
1712
1327
|
}
|
|
1713
|
-
return out;
|
|
1714
1328
|
}
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
const
|
|
1720
|
-
|
|
1721
|
-
const
|
|
1722
|
-
const
|
|
1723
|
-
|
|
1724
|
-
const
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1329
|
+
|
|
1330
|
+
// src/linker/odata-path-normalizer.ts
|
|
1331
|
+
function normalizeODataOperationInvocationPath(path11) {
|
|
1332
|
+
if (path11 === void 0) return void 0;
|
|
1333
|
+
const raw = path11.trim();
|
|
1334
|
+
if (!raw) return void 0;
|
|
1335
|
+
const rejected = (reason) => ({ rawOperationPath: raw, normalizedOperationPath: raw, wasInvocation: false, invocationArgumentPlaceholderKeys: [], normalizationRejectedReason: reason });
|
|
1336
|
+
const open = raw.indexOf("(");
|
|
1337
|
+
if (open < 0) return rejected("no_top_level_parenthesis");
|
|
1338
|
+
const query = topLevelQueryIndex(raw);
|
|
1339
|
+
if (query >= 0) return rejected("query_string_paths_are_not_operation_invocations");
|
|
1340
|
+
if (!raw.startsWith("/")) return rejected("path_is_not_absolute");
|
|
1341
|
+
if (raw.slice(1, open).includes("/")) return rejected("operation_segment_contains_navigation_separator");
|
|
1342
|
+
const close = matchingClose(raw, open);
|
|
1343
|
+
if (close === void 0) return rejected("top_level_invocation_parenthesis_is_unbalanced");
|
|
1344
|
+
if (raw.slice(close + 1).trim().length > 0) return rejected("top_level_invocation_does_not_cover_remaining_path");
|
|
1345
|
+
const operationSegment = raw.slice(0, open).trim();
|
|
1346
|
+
if (operationSegment.length <= 1) return rejected("operation_segment_is_empty");
|
|
1347
|
+
return {
|
|
1348
|
+
rawOperationPath: raw,
|
|
1349
|
+
normalizedOperationPath: operationSegment,
|
|
1350
|
+
wasInvocation: true,
|
|
1351
|
+
invocationArguments: raw.slice(open + 1, close),
|
|
1352
|
+
invocationArgumentPlaceholderKeys: [...new Set(extractTemplatePlaceholders(raw.slice(open + 1, close)))],
|
|
1353
|
+
normalizationReason: "balanced_top_level_operation_invocation"
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
function classifyODataPathIntent(path11, method) {
|
|
1357
|
+
const rawPath = (path11 ?? "").trim();
|
|
1358
|
+
const normalizedMethod = (method ?? "GET").trim().toUpperCase() || "GET";
|
|
1359
|
+
const queryIndex = rawPath.indexOf("?");
|
|
1360
|
+
const pathWithoutQuery = queryIndex >= 0 ? rawPath.slice(0, queryIndex) : rawPath;
|
|
1361
|
+
const queryString = queryIndex >= 0 ? rawPath.slice(queryIndex + 1) : void 0;
|
|
1362
|
+
const segments = pathWithoutQuery.replace(/^\//, "").split("/").filter(Boolean);
|
|
1363
|
+
const firstSegment = segments[0] ?? "";
|
|
1364
|
+
const hasNavigationSegments = segments.length > 1;
|
|
1365
|
+
const entitySegment = entitySegmentFromPath(pathWithoutQuery);
|
|
1366
|
+
const placeholderKeys2 = [...new Set(extractTemplatePlaceholders(rawPath))];
|
|
1367
|
+
const firstOpen = firstSegment.indexOf("(");
|
|
1368
|
+
const firstClose = firstOpen >= 0 ? matchingClose(firstSegment, firstOpen) : void 0;
|
|
1369
|
+
const keyPredicateText = firstOpen >= 0 && firstClose !== void 0 ? firstSegment.slice(firstOpen + 1, firstClose) : "";
|
|
1370
|
+
const rawKeyPredicatePlaceholderKeys = [...new Set(extractTemplatePlaceholders(keyPredicateText))];
|
|
1371
|
+
const navigationSuffix = hasNavigationSegments ? segments.slice(1).join("/") : void 0;
|
|
1372
|
+
const lastSegment = segments.at(-1) ?? "";
|
|
1373
|
+
const mediaOrPropertySuffix = hasNavigationSegments ? lastSegment : void 0;
|
|
1374
|
+
const invocation = normalizeODataOperationInvocationPath(pathWithoutQuery);
|
|
1375
|
+
const operationNameFromInvocation = invocation?.wasInvocation ? invocation.normalizedOperationPath.replace(/^\//, "").split(".").at(-1) : void 0;
|
|
1376
|
+
const topLevelName = firstSegment.split("(")[0]?.replace(/^\//, "");
|
|
1377
|
+
const topLevelOperationName = operationNameFromInvocation ?? (!hasNavigationSegments && !firstSegment.includes("(") ? topLevelName : void 0);
|
|
1378
|
+
const topLevelOperationInvocation = Boolean(invocation?.wasInvocation && looksLikeLowerCamelInvocation(firstSegment));
|
|
1379
|
+
const keyPredicatePlaceholderKeys = topLevelOperationInvocation ? [] : rawKeyPredicatePlaceholderKeys;
|
|
1380
|
+
const topLevelOperationNameCandidate = Boolean(topLevelOperationName && !hasNavigationSegments && !firstSegment.includes("("));
|
|
1381
|
+
const base = { rawPath, method: normalizedMethod, pathWithoutQuery, queryString, hasQueryString: queryIndex >= 0, entitySegment, placeholderKeys: placeholderKeys2, keyPredicatePlaceholderKeys, invocationArguments: invocation?.invocationArguments, invocationArgumentPlaceholderKeys: invocation?.invocationArgumentPlaceholderKeys ?? [], navigationSuffix, mediaOrPropertySuffix, hasEntityKeyPredicate: firstOpen >= 0 && firstClose !== void 0, hasNavigationSuffix: hasNavigationSegments, hasMediaOrPropertySuffix: Boolean(mediaOrPropertySuffix && isMediaOrPropertySuffix(mediaOrPropertySuffix)), topLevelOperationName, topLevelOperationNameCandidate, topLevelOperationInvocation };
|
|
1382
|
+
if (!rawPath || !rawPath.startsWith("/")) return { ...base, kind: "unknown", reason: "path_missing_or_not_absolute" };
|
|
1383
|
+
const upperEntityLike = /^[A-Z][A-Za-z0-9_]*$/.test(entitySegment ?? firstSegment);
|
|
1384
|
+
const mediaLike = isMediaOrPropertySuffix(segments.at(-1) ?? "");
|
|
1385
|
+
if (normalizedMethod !== "GET") {
|
|
1386
|
+
if (invocation?.wasInvocation && looksLikeLowerCamelInvocation(firstSegment)) return { ...base, kind: "operation_invocation", reason: "non_get_balanced_top_level_operation_invocation" };
|
|
1387
|
+
if (mediaLike) return { ...base, kind: "entity_media", reason: "non_get_entity_media_stream_path" };
|
|
1388
|
+
if (hasNavigationSegments || firstSegment.includes("(")) return { ...base, kind: normalizedMethod === "DELETE" ? "entity_delete" : "entity_mutation", reason: firstSegment.includes("(") ? "non_get_entity_key_or_navigation_path_shape" : "non_get_entity_navigation_path_shape" };
|
|
1389
|
+
if (upperEntityLike) return { ...base, kind: normalizedMethod === "DELETE" ? "entity_delete" : "entity_mutation", reason: "non_get_entity_path_shape" };
|
|
1390
|
+
return { ...base, kind: "operation_invocation", reason: "non_get_lowercase_path_may_be_operation" };
|
|
1747
1391
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
if (
|
|
1751
|
-
|
|
1752
|
-
helperCache.set(
|
|
1753
|
-
imp.sourceFile,
|
|
1754
|
-
await helperBindings(repoPath, imp.sourceFile)
|
|
1755
|
-
);
|
|
1756
|
-
return (helperCache.get(imp.sourceFile) ?? []).filter((h) => h.exportedName === imp.exportedName).map((helper) => ({ imp, helper }));
|
|
1392
|
+
if (queryIndex >= 0) {
|
|
1393
|
+
if (hasNavigationSegments) return { ...base, kind: "entity_navigation_query", reason: "get_path_has_navigation_and_query_string" };
|
|
1394
|
+
if (looksLikeLowerCamelInvocation(firstSegment)) return { ...base, kind: "unknown", reason: "get_invocation_with_query_string_requires_indexed_operation_evidence" };
|
|
1395
|
+
return { ...base, kind: "entity_query", reason: "get_collection_path_has_query_string" };
|
|
1757
1396
|
}
|
|
1758
|
-
|
|
1759
|
-
|
|
1397
|
+
if (hasNavigationSegments) return mediaLike ? { ...base, kind: "entity_media", reason: "get_entity_media_stream_path" } : { ...base, kind: "entity_navigation_query", reason: "get_path_has_navigation_segments" };
|
|
1398
|
+
if (firstSegment.includes("(")) {
|
|
1399
|
+
if (invocation?.wasInvocation && looksLikeLowerCamelInvocation(firstSegment)) return { ...base, kind: "operation_invocation", reason: "get_balanced_top_level_operation_invocation" };
|
|
1400
|
+
return looksLikeLowerCamelInvocation(firstSegment) ? { ...base, kind: "operation_invocation", reason: "get_single_lower_camel_segment_has_top_level_invocation" } : { ...base, kind: "entity_key_read", reason: "get_entity_segment_has_key_predicate" };
|
|
1760
1401
|
}
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1402
|
+
if (/^[A-Z][A-Za-z0-9_]*$/.test(firstSegment)) return { ...base, kind: "entity_candidate", reason: "uppercase_collection_segment_without_indexed_entity_evidence" };
|
|
1403
|
+
return { ...base, kind: "unknown", reason: "get_path_has_no_query_key_or_navigation_signal" };
|
|
1404
|
+
}
|
|
1405
|
+
function entitySegmentFromPath(path11) {
|
|
1406
|
+
const first = path11.replace(/^\//, "").split("/")[0]?.trim();
|
|
1407
|
+
if (!first) return void 0;
|
|
1408
|
+
const open = first.indexOf("(");
|
|
1409
|
+
const entity = (open >= 0 ? first.slice(0, open) : first).trim();
|
|
1410
|
+
return entity || void 0;
|
|
1411
|
+
}
|
|
1412
|
+
function isMediaOrPropertySuffix(segment) {
|
|
1413
|
+
return ["file", "content", "$value", "metadata", "items"].includes(segment.toLowerCase());
|
|
1414
|
+
}
|
|
1415
|
+
function looksLikeLowerCamelInvocation(segment) {
|
|
1416
|
+
const open = segment.indexOf("(");
|
|
1417
|
+
if (open <= 0) return false;
|
|
1418
|
+
const name = segment.slice(0, open).split(".").at(-1) ?? segment.slice(0, open);
|
|
1419
|
+
return /^[a-z][A-Za-z0-9_]*$/.test(name);
|
|
1420
|
+
}
|
|
1421
|
+
function extractTemplatePlaceholders(text) {
|
|
1422
|
+
const keys = [];
|
|
1423
|
+
for (let index = 0; index < text.length - 1; index += 1) {
|
|
1424
|
+
if (text[index] !== "$" || text[index + 1] !== "{") continue;
|
|
1425
|
+
const close = matchingPlaceholderClose(text, index + 1);
|
|
1426
|
+
if (close === void 0) continue;
|
|
1427
|
+
const key = text.slice(index + 2, close).trim();
|
|
1428
|
+
if (key) keys.push(key);
|
|
1429
|
+
index = close;
|
|
1764
1430
|
}
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1431
|
+
return keys;
|
|
1432
|
+
}
|
|
1433
|
+
function matchingClose(text, openIndex) {
|
|
1434
|
+
let depth = 0;
|
|
1435
|
+
let quote;
|
|
1436
|
+
for (let index = openIndex; index < text.length; index += 1) {
|
|
1437
|
+
const char = text[index];
|
|
1438
|
+
const prev = text[index - 1];
|
|
1439
|
+
if (quote) {
|
|
1440
|
+
if (prev === "\\") continue;
|
|
1441
|
+
if (quote === "template" && char === "$" && text[index + 1] === "{") {
|
|
1442
|
+
const close = matchingPlaceholderClose(text, index + 1);
|
|
1443
|
+
if (close === void 0) return void 0;
|
|
1444
|
+
index = close;
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
1447
|
+
if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
if (char === "$" && text[index + 1] === "{") {
|
|
1451
|
+
const close = matchingPlaceholderClose(text, index + 1);
|
|
1452
|
+
if (close === void 0) return void 0;
|
|
1453
|
+
index = close;
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
if (char === "'") {
|
|
1457
|
+
quote = "single";
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
if (char === '"') {
|
|
1461
|
+
quote = "double";
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
if (char === "`") {
|
|
1465
|
+
quote = "template";
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
if (char === "(") depth += 1;
|
|
1469
|
+
if (char === ")") {
|
|
1470
|
+
depth -= 1;
|
|
1471
|
+
if (depth === 0) return index;
|
|
1472
|
+
if (depth < 0) return void 0;
|
|
1473
|
+
}
|
|
1783
1474
|
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1475
|
+
return void 0;
|
|
1476
|
+
}
|
|
1477
|
+
function matchingPlaceholderClose(text, openBraceIndex) {
|
|
1478
|
+
let depth = 0;
|
|
1479
|
+
let quote;
|
|
1480
|
+
for (let index = openBraceIndex; index < text.length; index += 1) {
|
|
1481
|
+
const char = text[index];
|
|
1482
|
+
const prev = text[index - 1];
|
|
1483
|
+
if (quote) {
|
|
1484
|
+
if (prev === "\\") continue;
|
|
1485
|
+
if (quote === "template" && char === "$" && text[index + 1] === "{") {
|
|
1486
|
+
depth += 1;
|
|
1487
|
+
index += 1;
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
if (char === "'") {
|
|
1494
|
+
quote = "single";
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
if (char === '"') {
|
|
1498
|
+
quote = "double";
|
|
1499
|
+
continue;
|
|
1500
|
+
}
|
|
1501
|
+
if (char === "`") {
|
|
1502
|
+
quote = "template";
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
if (char === "{") depth += 1;
|
|
1506
|
+
if (char === "}") {
|
|
1507
|
+
depth -= 1;
|
|
1508
|
+
if (depth === 0) return index;
|
|
1509
|
+
if (depth < 0) return void 0;
|
|
1510
|
+
}
|
|
1789
1511
|
}
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
if (
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
placeholders: resolved2.helper.placeholders,
|
|
1814
|
-
sourceFile: normalizePath(filePath),
|
|
1815
|
-
sourceLine: lineOf4(sourceFileAst, node),
|
|
1816
|
-
helperChain: [
|
|
1817
|
-
...resolved2.helper.helperChain ?? [],
|
|
1818
|
-
{
|
|
1819
|
-
callerVariable: targetName,
|
|
1820
|
-
...aliasKind === "assignment" ? { assignedFrom: call.expression.text, aliasKind, scopeRule: "same-file-source-order" } : {},
|
|
1821
|
-
importedHelper: call.expression.text,
|
|
1822
|
-
importSource: resolved2.imp?.sourceFile,
|
|
1823
|
-
exportedSymbol: resolved2.imp?.exportedName ?? resolved2.helper.exportedName,
|
|
1824
|
-
helperSourceFile: resolved2.helper.sourceFile,
|
|
1825
|
-
helperSourceLine: resolved2.helper.sourceLine
|
|
1826
|
-
}
|
|
1827
|
-
]
|
|
1828
|
-
});
|
|
1512
|
+
return void 0;
|
|
1513
|
+
}
|
|
1514
|
+
function topLevelQueryIndex(text) {
|
|
1515
|
+
let quote;
|
|
1516
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1517
|
+
const char = text[index];
|
|
1518
|
+
const prev = text[index - 1];
|
|
1519
|
+
if (quote) {
|
|
1520
|
+
if (prev === "\\") continue;
|
|
1521
|
+
if (quote === "template" && char === "$" && text[index + 1] === "{") {
|
|
1522
|
+
const close = matchingPlaceholderClose(text, index + 1);
|
|
1523
|
+
if (close === void 0) return -1;
|
|
1524
|
+
index = close;
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
|
|
1528
|
+
continue;
|
|
1529
|
+
}
|
|
1530
|
+
if (char === "$" && text[index + 1] === "{") {
|
|
1531
|
+
const close = matchingPlaceholderClose(text, index + 1);
|
|
1532
|
+
if (close === void 0) return -1;
|
|
1533
|
+
index = close;
|
|
1534
|
+
continue;
|
|
1829
1535
|
}
|
|
1536
|
+
if (char === "'") {
|
|
1537
|
+
quote = "single";
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
if (char === '"') {
|
|
1541
|
+
quote = "double";
|
|
1542
|
+
continue;
|
|
1543
|
+
}
|
|
1544
|
+
if (char === "`") {
|
|
1545
|
+
quote = "template";
|
|
1546
|
+
continue;
|
|
1547
|
+
}
|
|
1548
|
+
if (char === "?") return index;
|
|
1830
1549
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1550
|
+
return -1;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// src/parsers/imported-wrapper-parser.ts
|
|
1554
|
+
import path9 from "path";
|
|
1555
|
+
import ts6 from "typescript";
|
|
1556
|
+
async function parseImportedWrapperCalls(repoPath, filePath, source, serviceBindings) {
|
|
1557
|
+
const imports = await importsFor(repoPath, filePath, source);
|
|
1558
|
+
const importedByLocal = new Map(imports.filter((item) => item.sourceFile).map((item) => [item.localName, item]));
|
|
1559
|
+
const calls = collectImportedCalls(source, importedByLocal);
|
|
1560
|
+
const out = [];
|
|
1561
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1562
|
+
for (const call of calls) {
|
|
1563
|
+
if (!ts6.isIdentifier(call.expression)) continue;
|
|
1564
|
+
const imported = importedByLocal.get(call.expression.text);
|
|
1565
|
+
if (!imported?.sourceFile) continue;
|
|
1566
|
+
const spec = await loadWrapperSpec(repoPath, imported, cache, 0);
|
|
1567
|
+
const fact = spec ? wrapperCallFact(source, filePath, call, spec, serviceBindings) : void 0;
|
|
1568
|
+
if (fact) out.push(fact);
|
|
1834
1569
|
}
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1570
|
+
return out;
|
|
1571
|
+
}
|
|
1572
|
+
function collectImportedCalls(source, imports) {
|
|
1573
|
+
const calls = [];
|
|
1574
|
+
const visit = (node) => {
|
|
1575
|
+
if (ts6.isCallExpression(node) && ts6.isIdentifier(node.expression) && imports.has(node.expression.text)) calls.push(node);
|
|
1576
|
+
ts6.forEachChild(node, visit);
|
|
1577
|
+
};
|
|
1578
|
+
visit(source);
|
|
1579
|
+
return calls;
|
|
1580
|
+
}
|
|
1581
|
+
async function loadWrapperSpec(repoPath, imported, cache, depth) {
|
|
1582
|
+
if (!imported.sourceFile || depth > 5) return void 0;
|
|
1583
|
+
const key = `${imported.sourceFile}#${imported.exportedName}`;
|
|
1584
|
+
const existing = cache.get(key);
|
|
1585
|
+
if (existing) return existing;
|
|
1586
|
+
const pending = inspectWrapper(repoPath, imported.sourceFile, imported.exportedName, cache, depth);
|
|
1587
|
+
cache.set(key, pending);
|
|
1588
|
+
return pending;
|
|
1589
|
+
}
|
|
1590
|
+
async function inspectWrapper(repoPath, sourceFile, exportedName, cache, depth) {
|
|
1591
|
+
const source = await readSource(path9.join(repoPath, sourceFile));
|
|
1592
|
+
if (!source) return void 0;
|
|
1593
|
+
const named = findFunction(source, exportedName);
|
|
1594
|
+
if (!named) return void 0;
|
|
1595
|
+
const direct = directSendSpec(source, sourceFile, named.name, named.fn);
|
|
1596
|
+
if (direct) return direct;
|
|
1597
|
+
return nestedSendSpec(repoPath, sourceFile, source, named.name, named.fn, cache, depth);
|
|
1598
|
+
}
|
|
1599
|
+
function directSendSpec(source, sourceFile, name, fn) {
|
|
1600
|
+
const params = parameterNames(fn);
|
|
1601
|
+
const sends = [];
|
|
1602
|
+
visitFunctionBody(fn, (node) => {
|
|
1603
|
+
if (ts6.isCallExpression(node) && ts6.isPropertyAccessExpression(node.expression) && node.expression.name.text === "send") sends.push(node);
|
|
1604
|
+
});
|
|
1605
|
+
if (sends.length !== 1) return void 0;
|
|
1606
|
+
const send = sends[0];
|
|
1607
|
+
const receiver = send && ts6.isPropertyAccessExpression(send.expression) && ts6.isIdentifier(send.expression.expression) ? send.expression.expression.text : void 0;
|
|
1608
|
+
const object = send?.arguments[0];
|
|
1609
|
+
if (!receiver || !object || !ts6.isObjectLiteralExpression(object)) return void 0;
|
|
1610
|
+
const pathExpr = propertyExpression(object, "path");
|
|
1611
|
+
const methodExpr = propertyExpression(object, "method");
|
|
1612
|
+
const pathName = pathExpr && ts6.isIdentifier(pathExpr) ? pathExpr.text : void 0;
|
|
1613
|
+
const clientIndex = params.indexOf(receiver);
|
|
1614
|
+
const pathIndex = pathName ? params.indexOf(pathName) : -1;
|
|
1615
|
+
if (clientIndex < 0 || pathIndex < 0) return void 0;
|
|
1616
|
+
const methodName = methodExpr && ts6.isIdentifier(methodExpr) ? methodExpr.text : void 0;
|
|
1617
|
+
const methodIndex = methodName ? params.indexOf(methodName) : -1;
|
|
1618
|
+
return { clientIndex, pathIndex, methodIndex: methodIndex >= 0 ? methodIndex : void 0, methodLiteral: literal(methodExpr), sourceFile, sourceLine: lineOf3(source, fn), chain: [name] };
|
|
1619
|
+
}
|
|
1620
|
+
async function nestedSendSpec(repoPath, sourceFile, source, name, fn, cache, depth) {
|
|
1621
|
+
const imports = await importsFor(repoPath, sourceFile, source);
|
|
1622
|
+
const byLocal = new Map(imports.filter((item) => item.sourceFile).map((item) => [item.localName, item]));
|
|
1623
|
+
const calls = [];
|
|
1624
|
+
visitFunctionBody(fn, (node) => {
|
|
1625
|
+
if (ts6.isCallExpression(node) && ts6.isIdentifier(node.expression) && byLocal.has(node.expression.text)) calls.push(node);
|
|
1626
|
+
});
|
|
1627
|
+
if (calls.length !== 1) return void 0;
|
|
1628
|
+
const call = calls[0];
|
|
1629
|
+
const imported = call && ts6.isIdentifier(call.expression) ? byLocal.get(call.expression.text) : void 0;
|
|
1630
|
+
const nested = imported ? await loadWrapperSpec(repoPath, imported, cache, depth + 1) : void 0;
|
|
1631
|
+
if (!call || !nested) return void 0;
|
|
1632
|
+
const params = parameterNames(fn);
|
|
1633
|
+
const clientIndex = mappedParameterIndex(call.arguments[nested.clientIndex], params);
|
|
1634
|
+
const pathIndex = mappedParameterIndex(call.arguments[nested.pathIndex], params);
|
|
1635
|
+
if (clientIndex < 0 || pathIndex < 0) return void 0;
|
|
1636
|
+
const methodIndex = nested.methodIndex === void 0 ? void 0 : mappedParameterIndex(call.arguments[nested.methodIndex], params);
|
|
1637
|
+
return { clientIndex, pathIndex, methodIndex: methodIndex !== void 0 && methodIndex >= 0 ? methodIndex : void 0, methodLiteral: nested.methodLiteral, sourceFile, sourceLine: lineOf3(source, fn), chain: [name, ...nested.chain] };
|
|
1638
|
+
}
|
|
1639
|
+
function wrapperCallFact(source, filePath, call, spec, serviceBindings) {
|
|
1640
|
+
const client = call.arguments[spec.clientIndex];
|
|
1641
|
+
if (!client || !ts6.isIdentifier(client) || !serviceBindings.has(client.text)) return void 0;
|
|
1642
|
+
const pathValue = resolveCallerPath(call.arguments[spec.pathIndex], call);
|
|
1643
|
+
const methodValue = spec.methodIndex === void 0 ? spec.methodLiteral : literal(call.arguments[spec.methodIndex]);
|
|
1644
|
+
const method = (methodValue ?? "POST").toUpperCase();
|
|
1645
|
+
const rawPath = pathValue.rawExpression;
|
|
1646
|
+
const operationPathExpr = pathValue.value ? normalizeOperationPath(pathValue.value) : runtimePathExpression(rawPath);
|
|
1647
|
+
return {
|
|
1648
|
+
callType: "remote_action",
|
|
1649
|
+
serviceVariableName: client.text,
|
|
1650
|
+
method,
|
|
1651
|
+
operationPathExpr,
|
|
1652
|
+
payloadSummary: call.getText(source),
|
|
1653
|
+
sourceFile: normalizePath(filePath),
|
|
1654
|
+
sourceLine: lineOf3(source, call),
|
|
1655
|
+
confidence: operationPathExpr ? 0.85 : 0.5,
|
|
1656
|
+
unresolvedReason: pathValue.value ? void 0 : "dynamic_operation_path_identifier",
|
|
1657
|
+
evidence: {
|
|
1658
|
+
parser: "typescript_ast",
|
|
1659
|
+
classifier: pathValue.value ? "imported_wrapper_literal_path" : "imported_wrapper_dynamic_path",
|
|
1660
|
+
receiver: client.text,
|
|
1661
|
+
wrapperFunction: spec.chain[0],
|
|
1662
|
+
wrapperChain: spec.chain,
|
|
1663
|
+
callerSite: { sourceFile: normalizePath(filePath), sourceLine: lineOf3(source, call) },
|
|
1664
|
+
calleeSite: { sourceFile: spec.sourceFile, sourceLine: spec.sourceLine },
|
|
1665
|
+
rawPathExpression: rawPath,
|
|
1666
|
+
missingPathIdentifier: pathValue.value ? void 0 : rawPath,
|
|
1667
|
+
odataPathIntent: pathValue.value ? classifyODataPathIntent(operationPathExpr, method) : void 0
|
|
1668
|
+
}
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
function resolveCallerPath(expr, use) {
|
|
1672
|
+
if (!expr) return { sourceKind: "missing", rawExpression: "" };
|
|
1673
|
+
if (ts6.isStringLiteralLike(expr) || ts6.isNoSubstitutionTemplateLiteral(expr)) return { value: expr.text, sourceKind: "literal", rawExpression: expr.getText() };
|
|
1674
|
+
if (ts6.isTemplateExpression(expr)) return { value: expr.getText().slice(1, -1), sourceKind: "template", rawExpression: expr.getText() };
|
|
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
|
+
}
|
|
1840
1681
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
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;
|
|
1694
|
+
}
|
|
1695
|
+
function findFunction(source, exportedName) {
|
|
1696
|
+
const localName = exportedLocalName(source, exportedName);
|
|
1697
|
+
for (const statement of source.statements) {
|
|
1698
|
+
if (ts6.isFunctionDeclaration(statement) && statement.name?.text === localName) return { name: exportedName, fn: statement };
|
|
1699
|
+
if (!ts6.isVariableStatement(statement)) continue;
|
|
1700
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1701
|
+
if (ts6.isIdentifier(declaration.name) && declaration.name.text === localName && declaration.initializer && (ts6.isArrowFunction(declaration.initializer) || ts6.isFunctionExpression(declaration.initializer))) return { name: exportedName, fn: declaration.initializer };
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
return void 0;
|
|
1705
|
+
}
|
|
1706
|
+
function exportedLocalName(source, exportedName) {
|
|
1707
|
+
for (const statement of source.statements) {
|
|
1708
|
+
if (!ts6.isExportDeclaration(statement) || !statement.exportClause || !ts6.isNamedExports(statement.exportClause)) continue;
|
|
1709
|
+
const match = statement.exportClause.elements.find((item) => item.name.text === exportedName);
|
|
1710
|
+
if (match) return match.propertyName?.text ?? match.name.text;
|
|
1711
|
+
}
|
|
1712
|
+
return exportedName;
|
|
1713
|
+
}
|
|
1714
|
+
function visitFunctionBody(fn, visitor) {
|
|
1715
|
+
const visit = (node) => {
|
|
1716
|
+
if (node !== fn && ts6.isFunctionLike(node)) return;
|
|
1717
|
+
visitor(node);
|
|
1718
|
+
ts6.forEachChild(node, visit);
|
|
1719
|
+
};
|
|
1720
|
+
if (fn.body) visit(fn.body);
|
|
1721
|
+
}
|
|
1722
|
+
function parameterNames(fn) {
|
|
1723
|
+
return fn.parameters.map((parameter) => ts6.isIdentifier(parameter.name) ? parameter.name.text : "");
|
|
1724
|
+
}
|
|
1725
|
+
function mappedParameterIndex(expr, parameters) {
|
|
1726
|
+
return expr && ts6.isIdentifier(expr) ? parameters.indexOf(expr.text) : -1;
|
|
1727
|
+
}
|
|
1728
|
+
function propertyExpression(object, key) {
|
|
1729
|
+
for (const property of object.properties) {
|
|
1730
|
+
if (ts6.isPropertyAssignment(property) && propertyName(property.name) === key) return property.initializer;
|
|
1731
|
+
if (ts6.isShorthandPropertyAssignment(property) && property.name.text === key) return property.name;
|
|
1732
|
+
}
|
|
1733
|
+
return void 0;
|
|
1734
|
+
}
|
|
1735
|
+
function propertyName(name) {
|
|
1736
|
+
return ts6.isIdentifier(name) || ts6.isStringLiteralLike(name) ? name.text : void 0;
|
|
1737
|
+
}
|
|
1738
|
+
function literal(expr) {
|
|
1739
|
+
return expr && (ts6.isStringLiteralLike(expr) || ts6.isNoSubstitutionTemplateLiteral(expr)) ? expr.text : void 0;
|
|
1740
|
+
}
|
|
1741
|
+
function normalizeOperationPath(value) {
|
|
1742
|
+
return value.startsWith("/") ? value : `/${value}`;
|
|
1743
|
+
}
|
|
1744
|
+
function runtimePathExpression(value) {
|
|
1745
|
+
return value ? `\${${value}}` : void 0;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// src/parsers/outbound-call-parser.ts
|
|
1749
|
+
function lineOf4(text, idx) {
|
|
1750
|
+
return text.slice(0, idx).split("\n").length;
|
|
1751
|
+
}
|
|
1752
|
+
function entityFromExpression(expr) {
|
|
1753
|
+
if (!expr) return void 0;
|
|
1754
|
+
if (ts7.isIdentifier(expr) || ts7.isStringLiteral(expr) || ts7.isNoSubstitutionTemplateLiteral(expr)) return expr.text;
|
|
1755
|
+
if (ts7.isPropertyAccessExpression(expr) && expr.expression.kind === ts7.SyntaxKind.ThisKeyword) return expr.name.text;
|
|
1756
|
+
if (ts7.isElementAccessExpression(expr) && expr.argumentExpression && (ts7.isStringLiteral(expr.argumentExpression) || ts7.isNoSubstitutionTemplateLiteral(expr.argumentExpression))) return expr.argumentExpression.text;
|
|
1757
|
+
return void 0;
|
|
1758
|
+
}
|
|
1759
|
+
function expressionName(expr) {
|
|
1760
|
+
if (ts7.isIdentifier(expr)) return expr.text;
|
|
1761
|
+
if (ts7.isPropertyAccessExpression(expr)) return `${expressionName(expr.expression)}.${expr.name.text}`;
|
|
1762
|
+
return expr.getText();
|
|
1763
|
+
}
|
|
1764
|
+
function variableInitializers(source) {
|
|
1765
|
+
const initializers = /* @__PURE__ */ new Map();
|
|
1766
|
+
for (const statement of source.statements) {
|
|
1767
|
+
if (!ts7.isVariableStatement(statement) || (statement.declarationList.flags & ts7.NodeFlags.Const) === 0) continue;
|
|
1768
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1769
|
+
if (ts7.isIdentifier(declaration.name) && declaration.initializer) initializers.set(declaration.name.text, declaration.initializer);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
return initializers;
|
|
1773
|
+
}
|
|
1774
|
+
function queryEntityFromAst(expr, initializers = /* @__PURE__ */ new Map()) {
|
|
1775
|
+
if (ts7.isParenthesizedExpression(expr) || ts7.isAwaitExpression(expr)) return queryEntityFromAst(expr.expression, initializers);
|
|
1776
|
+
if (ts7.isIdentifier(expr) && initializers.has(expr.text)) return queryEntityFromAst(initializers.get(expr.text), initializers);
|
|
1777
|
+
if (ts7.isCallExpression(expr)) {
|
|
1778
|
+
const name = expressionName(expr.expression);
|
|
1779
|
+
if (name === "cds.run") return queryEntityFromAst(expr.arguments[0], initializers);
|
|
1780
|
+
if (["SELECT.one.from", "SELECT.from", "SELECT.one", "INSERT.into", "UPSERT.into", "DELETE.from", "UPDATE.entity"].includes(name)) return entityFromExpression(expr.arguments[0]);
|
|
1781
|
+
if (name === "UPDATE") return entityFromExpression(expr.arguments[0]);
|
|
1782
|
+
const receiver = ts7.isPropertyAccessExpression(expr.expression) ? expr.expression.expression : void 0;
|
|
1783
|
+
if (receiver) return queryEntityFromAst(receiver, initializers);
|
|
1784
|
+
}
|
|
1785
|
+
return void 0;
|
|
1786
|
+
}
|
|
1787
|
+
function extractQueryEntity(expr) {
|
|
1788
|
+
const source = ts7.createSourceFile("query.ts", `const __query = (${expr});`, ts7.ScriptTarget.Latest, true, ts7.ScriptKind.TS);
|
|
1789
|
+
const initializers = variableInitializers(source);
|
|
1790
|
+
let found;
|
|
1791
|
+
const visit = (node) => {
|
|
1792
|
+
if (found) return;
|
|
1793
|
+
if (ts7.isParenthesizedExpression(node)) found = queryEntityFromAst(node.expression, initializers);
|
|
1794
|
+
ts7.forEachChild(node, visit);
|
|
1795
|
+
};
|
|
1796
|
+
visit(source);
|
|
1797
|
+
return found;
|
|
1798
|
+
}
|
|
1799
|
+
function queryWarning(expr) {
|
|
1800
|
+
if (/^\s*[`'"]/.test(expr)) return "raw_sql_or_cql_expression";
|
|
1801
|
+
if (/^\s*\w+\s*$/.test(expr)) return "query_variable_without_static_initializer";
|
|
1802
|
+
return "dynamic_entity_expression";
|
|
1803
|
+
}
|
|
1804
|
+
function parserEvidence(source, node, extra) {
|
|
1805
|
+
return { parser: "typescript_ast", startOffset: node.getStart(source), endOffset: node.getEnd(), ...extra };
|
|
1806
|
+
}
|
|
1807
|
+
function isStringLike(expr) {
|
|
1808
|
+
return Boolean(expr && (ts7.isStringLiteral(expr) || ts7.isNoSubstitutionTemplateLiteral(expr)));
|
|
1809
|
+
}
|
|
1810
|
+
function literalText(expr) {
|
|
1811
|
+
if (isStringLike(expr)) return expr.text;
|
|
1812
|
+
return void 0;
|
|
1813
|
+
}
|
|
1814
|
+
function objectPropertyText(object, key) {
|
|
1815
|
+
const prop = object.properties.find(
|
|
1816
|
+
(property) => ts7.isPropertyAssignment(property) && nameOfProperty(property.name) === key || ts7.isShorthandPropertyAssignment(property) && property.name.text === key
|
|
1817
|
+
);
|
|
1818
|
+
if (!prop) return void 0;
|
|
1819
|
+
return ts7.isShorthandPropertyAssignment(prop) ? prop.name.text : prop.initializer.getText();
|
|
1820
|
+
}
|
|
1821
|
+
function objectPropertyIsShorthand(object, key) {
|
|
1822
|
+
return object.properties.some((property) => ts7.isShorthandPropertyAssignment(property) && property.name.text === key);
|
|
1823
|
+
}
|
|
1824
|
+
function nameOfProperty(name) {
|
|
1825
|
+
if (ts7.isIdentifier(name) || ts7.isStringLiteral(name) || ts7.isNumericLiteral(name)) return name.text;
|
|
1826
|
+
return void 0;
|
|
1827
|
+
}
|
|
1828
|
+
var maxAliasDepth = 5;
|
|
1829
|
+
function safeRaw(expr) {
|
|
1830
|
+
if (ts7.isStringLiteral(expr) || ts7.isNoSubstitutionTemplateLiteral(expr) || ts7.isIdentifier(expr) || ts7.isTemplateExpression(expr)) return expr.getText(expr.getSourceFile());
|
|
1831
|
+
return void 0;
|
|
1832
|
+
}
|
|
1833
|
+
function placeholders2(expr) {
|
|
1834
|
+
return expr.templateSpans.map((span) => span.expression.getText(expr.getSourceFile()));
|
|
1835
|
+
}
|
|
1836
|
+
function isFunctionLikeScope(node) {
|
|
1837
|
+
return ts7.isFunctionLike(node) || ts7.isSourceFile(node);
|
|
1838
|
+
}
|
|
1839
|
+
function nodeContains(parent, child) {
|
|
1840
|
+
const source = child.getSourceFile();
|
|
1841
|
+
return child.getStart(source) >= parent.getStart(source) && child.getEnd() <= parent.getEnd();
|
|
1842
|
+
}
|
|
1843
|
+
function declarationScope(node) {
|
|
1844
|
+
if (ts7.isParameter(node)) return node.parent;
|
|
1845
|
+
if (ts7.isCatchClause(node.parent) && node.parent.variableDeclaration === node) return node.parent;
|
|
1846
|
+
const list = node.parent;
|
|
1847
|
+
const blockScoped = (list.flags & (ts7.NodeFlags.Const | ts7.NodeFlags.Let)) !== 0;
|
|
1848
|
+
let current = list.parent;
|
|
1849
|
+
if (!blockScoped) {
|
|
1850
|
+
while (current.parent && !isFunctionLikeScope(current)) current = current.parent;
|
|
1851
|
+
return current;
|
|
1852
|
+
}
|
|
1853
|
+
while (current.parent && !ts7.isBlock(current) && !ts7.isSourceFile(current) && !ts7.isModuleBlock(current) && !ts7.isCaseBlock(current) && !isLoopInitializerScope(node, current) && !isFunctionLikeScope(current)) current = current.parent;
|
|
1854
|
+
return current;
|
|
1855
|
+
}
|
|
1856
|
+
function isLoopInitializerScope(declaration, scope) {
|
|
1857
|
+
const list = declaration.parent;
|
|
1858
|
+
return ts7.isForStatement(scope) && scope.initializer === list || (ts7.isForInStatement(scope) || ts7.isForOfStatement(scope)) && scope.initializer === list;
|
|
1859
|
+
}
|
|
1860
|
+
function catchBindingScope(declaration) {
|
|
1861
|
+
if (ts7.isParameter(declaration)) return void 0;
|
|
1862
|
+
return ts7.isCatchClause(declaration.parent) && declaration.parent.variableDeclaration === declaration ? declaration.parent : void 0;
|
|
1863
|
+
}
|
|
1864
|
+
function isAccessibleDeclaration(declaration, use) {
|
|
1865
|
+
const source = use.getSourceFile();
|
|
1866
|
+
if (declaration.name.getStart(source) >= use.getStart(source)) return false;
|
|
1867
|
+
const catchScope = catchBindingScope(declaration);
|
|
1868
|
+
if (catchScope) return nodeContains(catchScope.block, use);
|
|
1869
|
+
const scope = declarationScope(declaration);
|
|
1870
|
+
if (ts7.isForStatement(scope) || ts7.isForInStatement(scope) || ts7.isForOfStatement(scope)) return nodeContains(scope.statement, use);
|
|
1871
|
+
return ts7.isSourceFile(scope) || nodeContains(scope, use);
|
|
1872
|
+
}
|
|
1873
|
+
function resolveBinding(identifier, use) {
|
|
1874
|
+
const source = use.getSourceFile();
|
|
1875
|
+
let best;
|
|
1876
|
+
const visit = (node) => {
|
|
1877
|
+
if (ts7.isVariableDeclaration(node) && ts7.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
|
|
1878
|
+
if (ts7.isParameter(node) && ts7.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
|
|
1879
|
+
ts7.forEachChild(node, visit);
|
|
1880
|
+
};
|
|
1881
|
+
visit(source);
|
|
1882
|
+
if (!best) return { immutable: false, evidence: ["binding_not_found"] };
|
|
1883
|
+
const immutable = ts7.isVariableDeclaration(best) && (best.parent.flags & ts7.NodeFlags.Const) !== 0;
|
|
1884
|
+
return { declaration: best, initializer: ts7.isVariableDeclaration(best) ? best.initializer : void 0, immutable, evidence: [immutable ? "lexical_const_binding_before_use" : "lexical_mutable_or_parameter_binding"] };
|
|
1885
|
+
}
|
|
1886
|
+
function resolveExpression(expr, use, policy, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
1887
|
+
if (!expr) return { status: "unknown", sourceKind: "dynamic_expression", placeholderKeys: [], evidence: ["expression_missing"] };
|
|
1888
|
+
if (ts7.isStringLiteral(expr)) return { status: "static", sourceKind: "string_literal", value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["string_literal"] };
|
|
1889
|
+
if (ts7.isNoSubstitutionTemplateLiteral(expr)) return { status: "static", sourceKind: "no_substitution_template", value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["no_substitution_template"] };
|
|
1890
|
+
if (ts7.isTemplateExpression(expr)) {
|
|
1891
|
+
const keys = placeholders2(expr);
|
|
1892
|
+
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
|
+
return { status: "dynamic", sourceKind: "template_with_substitutions", placeholderKeys: keys, evidence: ["template_substitutions_not_static_external_target"] };
|
|
1894
|
+
}
|
|
1895
|
+
if (ts7.isIdentifier(expr)) {
|
|
1896
|
+
if (depth >= maxAliasDepth) return { status: "unknown", sourceKind: "const_alias", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["alias_depth_exceeded"], constName: expr.text };
|
|
1897
|
+
const binding = resolveBinding(expr, use);
|
|
1898
|
+
if (!binding.declaration || !binding.initializer || !binding.immutable) return { status: "dynamic", sourceKind: "dynamic_expression", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: binding.evidence, constName: expr.text };
|
|
1899
|
+
if (seen.has(binding.declaration)) return { status: "unknown", sourceKind: "const_alias", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["alias_cycle_detected"], constName: expr.text };
|
|
1900
|
+
seen.add(binding.declaration);
|
|
1901
|
+
const resolved2 = resolveExpression(binding.initializer, binding.declaration, policy, depth + 1, seen);
|
|
1902
|
+
return { ...resolved2, sourceKind: "const_alias", rawExpression: safeRaw(expr), constName: expr.text, evidence: [...binding.evidence, ...resolved2.evidence] };
|
|
1903
|
+
}
|
|
1904
|
+
return { status: "dynamic", sourceKind: "dynamic_expression", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: [`unsupported_${ts7.SyntaxKind[expr.kind] ?? "expression"}`] };
|
|
1905
|
+
}
|
|
1906
|
+
function staticExpressionText(expr, initializers) {
|
|
1907
|
+
if (!expr) return void 0;
|
|
1908
|
+
if (isStringLike(expr)) return expr.text;
|
|
1909
|
+
if (ts7.isIdentifier(expr) && initializers.has(expr.text)) return staticExpressionText(initializers.get(expr.text), initializers);
|
|
1910
|
+
return void 0;
|
|
1911
|
+
}
|
|
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
|
+
function operationPathFromStatic(text) {
|
|
1918
|
+
return text ? `/${stripQuotes(text).replace(/^\//, "")}` : void 0;
|
|
1919
|
+
}
|
|
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
|
+
function destinationExpressionShape(expr) {
|
|
1949
|
+
if (!expr) return void 0;
|
|
1950
|
+
if (ts7.isIdentifier(expr)) return "identifier";
|
|
1951
|
+
if (ts7.isPropertyAccessExpression(expr) || ts7.isElementAccessExpression(expr)) return "property_read";
|
|
1952
|
+
if (ts7.isCallExpression(expr)) return "function_call";
|
|
1953
|
+
if (ts7.isConditionalExpression(expr)) return "conditional";
|
|
1954
|
+
if (ts7.isBinaryExpression(expr)) return "binary_expression";
|
|
1955
|
+
if (ts7.isTemplateExpression(expr)) return "template_expression";
|
|
1956
|
+
return ts7.SyntaxKind[expr.kind] ?? "expression";
|
|
1957
|
+
}
|
|
1958
|
+
function staticConditionalCandidates(expr, initializers) {
|
|
1959
|
+
const resolved2 = expr && ts7.isIdentifier(expr) && initializers.has(expr.text) ? initializers.get(expr.text) : expr;
|
|
1960
|
+
if (!resolved2 || !ts7.isConditionalExpression(resolved2)) return void 0;
|
|
1961
|
+
const left = staticExpressionText(resolved2.whenTrue, initializers);
|
|
1962
|
+
const right = staticExpressionText(resolved2.whenFalse, initializers);
|
|
1963
|
+
if (!left || !right) return void 0;
|
|
1964
|
+
return [.../* @__PURE__ */ new Set([left, right])];
|
|
1965
|
+
}
|
|
1966
|
+
function propertyInitializer(object, key) {
|
|
1967
|
+
for (const property of object.properties) {
|
|
1968
|
+
if (ts7.isPropertyAssignment(property) && nameOfProperty(property.name) === key) return property.initializer;
|
|
1969
|
+
if (ts7.isShorthandPropertyAssignment(property) && property.name.text === key) return property.name;
|
|
1970
|
+
}
|
|
1971
|
+
return void 0;
|
|
1972
|
+
}
|
|
1973
|
+
function httpMethodFromObject(object, use) {
|
|
1974
|
+
const text = resolveExpression(propertyInitializer(object, "method"), use, "literal").value;
|
|
1975
|
+
return text ? stripQuotes(text).toUpperCase() : void 0;
|
|
1976
|
+
}
|
|
1977
|
+
var supportedHttpMethods = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
|
|
1978
|
+
function safeOperationName(value) {
|
|
1979
|
+
if (!value || !/^[A-Za-z_$][\w$]*(?:[./][A-Za-z_$][\w$]*)*$/.test(value)) return void 0;
|
|
1980
|
+
return operationPathFromStatic(value);
|
|
1981
|
+
}
|
|
1982
|
+
function hasTemplatePlaceholder(value) {
|
|
1983
|
+
return /\$\{|%7B|%7D/i.test(value);
|
|
1984
|
+
}
|
|
1985
|
+
function urlTargetFromExpression(expr, use) {
|
|
1986
|
+
const resolved2 = resolveExpression(expr, use, "external");
|
|
1987
|
+
if (resolved2.status === "static" && resolved2.value && !hasTemplatePlaceholder(resolved2.value)) return { kind: "static_url", expression: resolved2.value, dynamic: false, sourceKind: resolved2.sourceKind };
|
|
1988
|
+
if (expr) return { kind: "url_expression", dynamic: true, expression: `${resolved2.sourceKind}:${resolved2.placeholderKeys.join("|")}`, expressionShape: resolved2.sourceKind, placeholderKeys: resolved2.placeholderKeys };
|
|
1989
|
+
return { kind: "unknown", dynamic: false };
|
|
1990
|
+
}
|
|
1991
|
+
function destinationTargetFromExpression(expr, use) {
|
|
1992
|
+
const resolved2 = resolveExpression(expr, use, "external");
|
|
1993
|
+
const text = resolved2.value;
|
|
1994
|
+
if (resolved2.status === "static" && text && !hasTemplatePlaceholder(text)) return { kind: "destination", expression: text, dynamic: false, sourceKind: resolved2.sourceKind };
|
|
1995
|
+
const candidates = staticConditionalCandidates(expr, /* @__PURE__ */ new Map());
|
|
1996
|
+
if (candidates) return { kind: "destination", dynamic: true, expressionShape: "conditional", candidateLiterals: candidates };
|
|
1997
|
+
const shape = destinationExpressionShape(expr);
|
|
1998
|
+
if (shape) return { kind: "destination", dynamic: true, expressionShape: shape };
|
|
1999
|
+
return void 0;
|
|
2000
|
+
}
|
|
2001
|
+
function externalHttpEvidence(node, source) {
|
|
2002
|
+
const expr = node.expression;
|
|
2003
|
+
const exprText = expr.getText(source);
|
|
2004
|
+
if (exprText === "useOrFetchDestination") {
|
|
2005
|
+
const objectArg = node.arguments[0];
|
|
2006
|
+
if (objectArg && ts7.isObjectLiteralExpression(objectArg)) {
|
|
2007
|
+
const destination = destinationTargetFromExpression(propertyInitializer(objectArg, "destinationName"), node);
|
|
2008
|
+
return { externalTarget: destination ?? { kind: "unknown", dynamic: false }, classifier: "sap_destination_lookup", sourceCallShape: "useOrFetchDestination" };
|
|
1865
2009
|
}
|
|
1866
2010
|
}
|
|
1867
|
-
|
|
1868
|
-
const
|
|
1869
|
-
|
|
1870
|
-
const
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
let propertyName;
|
|
1874
|
-
let targetName;
|
|
1875
|
-
if (ts5.isShorthandPropertyAssignment(prop)) {
|
|
1876
|
-
propertyName = prop.name.text;
|
|
1877
|
-
targetName = prop.name.text;
|
|
1878
|
-
} else if (ts5.isPropertyAssignment(prop)) {
|
|
1879
|
-
propertyName = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
1880
|
-
targetName = ts5.isIdentifier(prop.initializer) ? prop.initializer.text : void 0;
|
|
1881
|
-
}
|
|
1882
|
-
if (!propertyName || !targetName) continue;
|
|
1883
|
-
const matches = helpers.filter((row) => row.helper.returnedProperty === propertyName);
|
|
1884
|
-
if (matches.length !== 1) continue;
|
|
1885
|
-
const resolved2 = matches[0];
|
|
1886
|
-
out.push({
|
|
1887
|
-
variableName: targetName,
|
|
1888
|
-
alias: resolved2.helper.alias,
|
|
1889
|
-
aliasExpr: resolved2.helper.aliasExpr,
|
|
1890
|
-
destinationExpr: resolved2.helper.destinationExpr,
|
|
1891
|
-
servicePathExpr: resolved2.helper.servicePathExpr,
|
|
1892
|
-
isDynamic: resolved2.helper.isDynamic,
|
|
1893
|
-
placeholders: resolved2.helper.placeholders,
|
|
1894
|
-
sourceFile: normalizePath(filePath),
|
|
1895
|
-
sourceLine: lineOf4(sourceFileAst, node),
|
|
1896
|
-
helperChain: [...resolved2.helper.helperChain ?? [], { callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind: "assignment", scopeRule: "same-file-source-order", returnedProperty: propertyName, importSource: resolved2.imp?.sourceFile, exportedSymbol: resolved2.imp?.exportedName, helperSourceFile: resolved2.helper.sourceFile, helperSourceLine: resolved2.helper.sourceLine }]
|
|
1897
|
-
});
|
|
1898
|
-
}
|
|
2011
|
+
if (exprText === "executeHttpRequest") {
|
|
2012
|
+
const destination = destinationTargetFromExpression(node.arguments[0], node);
|
|
2013
|
+
const config = node.arguments[1];
|
|
2014
|
+
const method = config && ts7.isObjectLiteralExpression(config) ? httpMethodFromObject(config, node) : void 0;
|
|
2015
|
+
const url = config && ts7.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, "url"), node) : { kind: "unknown", dynamic: false };
|
|
2016
|
+
return { method, externalTarget: destination ? { ...url, destination } : url, classifier: "sap_execute_http_request", sourceCallShape: "executeHttpRequest" };
|
|
1899
2017
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
if (target.expression.kind !== ts5.SyntaxKind.ThisKeyword) return;
|
|
1906
|
-
for (const el of decl.name.elements) {
|
|
1907
|
-
if (!ts5.isIdentifier(el.name)) continue;
|
|
1908
|
-
const propertyName = el.propertyName && ts5.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
|
|
1909
|
-
const helper = classHelpers.find(
|
|
1910
|
-
(h) => h.helperName === target.name.text && h.propertyName === propertyName
|
|
1911
|
-
);
|
|
1912
|
-
if (!helper) continue;
|
|
1913
|
-
out.push({
|
|
1914
|
-
variableName: el.name.text,
|
|
1915
|
-
...helper.fact,
|
|
1916
|
-
sourceFile: normalizePath(filePath),
|
|
1917
|
-
sourceLine: lineOf4(sourceFileAst, decl),
|
|
1918
|
-
helperChain: [
|
|
1919
|
-
{
|
|
1920
|
-
callerVariable: el.name.text,
|
|
1921
|
-
className: helper.className,
|
|
1922
|
-
classHelper: helper.helperName,
|
|
1923
|
-
returnedProperty: helper.propertyName,
|
|
1924
|
-
helperVariable: helper.variableName,
|
|
1925
|
-
helperSourceFile: normalizePath(filePath),
|
|
1926
|
-
helperSourceLine: helper.sourceLine
|
|
1927
|
-
}
|
|
1928
|
-
]
|
|
1929
|
-
});
|
|
2018
|
+
if (exprText === "axios") {
|
|
2019
|
+
const config = node.arguments[0];
|
|
2020
|
+
if (config && ts7.isObjectLiteralExpression(config)) {
|
|
2021
|
+
const method = httpMethodFromObject(config, node);
|
|
2022
|
+
return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, "url"), node), classifier: "axios_config_call", sourceCallShape: "axios(config)" };
|
|
1930
2023
|
}
|
|
2024
|
+
return { externalTarget: { kind: "unknown", dynamic: false }, classifier: "axios_unknown_call", sourceCallShape: "axios(...)" };
|
|
1931
2025
|
}
|
|
1932
|
-
|
|
1933
|
-
const
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
if (!call) return void 0;
|
|
1937
|
-
if (!ts5.isPropertyAccessExpression(call.expression) || call.expression.name.text !== "all" || call.expression.expression.getText(sourceFileAst) !== "Promise") return void 0;
|
|
1938
|
-
const first = call.arguments[0];
|
|
1939
|
-
if (!first) return void 0;
|
|
1940
|
-
const container = unwrapIdentityExpression(first);
|
|
1941
|
-
if (!ts5.isArrayLiteralExpression(container)) return void 0;
|
|
1942
|
-
return { elements: container.elements, promiseAll: true };
|
|
2026
|
+
if (exprText === "fetch") {
|
|
2027
|
+
const init = node.arguments[1];
|
|
2028
|
+
const method = init && ts7.isObjectLiteralExpression(init) ? httpMethodFromObject(init, node) : void 0;
|
|
2029
|
+
return { method, externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: "fetch_call", sourceCallShape: "fetch" };
|
|
1943
2030
|
}
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
await recordBindingFromExpression(targetName, expr, node, "declaration");
|
|
1947
|
-
if (out.length > before) {
|
|
1948
|
-
const row = out[out.length - 1];
|
|
1949
|
-
row.helperChain = [
|
|
1950
|
-
...row.helperChain ?? [],
|
|
1951
|
-
{ callerVariable: targetName, targetVariable: targetName, arrayIndex, promiseAll, arrayContainer: promiseAll ? "Promise.all" : "array_literal" }
|
|
1952
|
-
];
|
|
1953
|
-
return;
|
|
1954
|
-
}
|
|
1955
|
-
const unwrapped = unwrapIdentityExpression(expr);
|
|
1956
|
-
if (ts5.isIdentifier(unwrapped)) {
|
|
1957
|
-
const existing = bindingForVariable(unwrapped.text);
|
|
1958
|
-
if (!existing) return;
|
|
1959
|
-
out.push({
|
|
1960
|
-
...existing,
|
|
1961
|
-
variableName: targetName,
|
|
1962
|
-
sourceLine: lineOf4(sourceFileAst, node),
|
|
1963
|
-
helperChain: [
|
|
1964
|
-
...existing.helperChain ?? [],
|
|
1965
|
-
{ callerVariable: targetName, targetVariable: targetName, sourceVariable: unwrapped.text, aliasKind: "array-destructuring", arrayIndex, promiseAll, arrayContainer: promiseAll ? "Promise.all" : "array_literal" }
|
|
1966
|
-
]
|
|
1967
|
-
});
|
|
1968
|
-
}
|
|
2031
|
+
if (ts7.isPropertyAccessExpression(expr) && ["get", "post", "put", "patch", "delete", "head"].includes(expr.name.text) && expr.expression.getText(source) === "axios") {
|
|
2032
|
+
return { method: expr.name.text.toUpperCase(), externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: "axios_member_call", sourceCallShape: `axios.${expr.name.text}` };
|
|
1969
2033
|
}
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
if (
|
|
1978
|
-
const source = container.elements[index];
|
|
1979
|
-
if (!source || ts5.isOmittedExpression(source)) continue;
|
|
1980
|
-
await recordArrayElementBinding(el.name.text, source, decl, index, container.promiseAll);
|
|
2034
|
+
return void 0;
|
|
2035
|
+
}
|
|
2036
|
+
function collectServiceVariables(source) {
|
|
2037
|
+
const vars = /* @__PURE__ */ new Set(["cds", "messaging", "messageClient", "eventClient"]);
|
|
2038
|
+
const visit = (node) => {
|
|
2039
|
+
if (ts7.isVariableDeclaration(node) && ts7.isIdentifier(node.name) && node.initializer) {
|
|
2040
|
+
const text = node.initializer.getText(source);
|
|
2041
|
+
if (/cds\.connect\.(to|messaging)\s*\(/.test(text)) vars.add(node.name.text);
|
|
1981
2042
|
}
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
2043
|
+
ts7.forEachChild(node, visit);
|
|
2044
|
+
};
|
|
2045
|
+
visit(source);
|
|
2046
|
+
return vars;
|
|
2047
|
+
}
|
|
2048
|
+
function receiverName(expr) {
|
|
2049
|
+
if (ts7.isIdentifier(expr)) return expr.text;
|
|
2050
|
+
if (ts7.isPropertyAccessExpression(expr)) return expr.getText(sourceOf(expr));
|
|
2051
|
+
return void 0;
|
|
2052
|
+
}
|
|
2053
|
+
function sourceOf(node) {
|
|
2054
|
+
return node.getSourceFile();
|
|
2055
|
+
}
|
|
2056
|
+
function rootReceiverName(expr) {
|
|
2057
|
+
if (ts7.isIdentifier(expr)) return expr.text;
|
|
2058
|
+
if (ts7.isPropertyAccessExpression(expr)) return rootReceiverName(expr.expression);
|
|
2059
|
+
if (ts7.isCallExpression(expr)) return rootReceiverName(expr.expression);
|
|
2060
|
+
return void 0;
|
|
2061
|
+
}
|
|
2062
|
+
function isSupportedEventReceiver(receiver, rootReceiver, serviceVariables) {
|
|
2063
|
+
const candidate = rootReceiver ?? receiver;
|
|
2064
|
+
if (!candidate) return false;
|
|
2065
|
+
if (candidate === "cds") return true;
|
|
2066
|
+
if (serviceVariables.has(candidate)) return true;
|
|
2067
|
+
if (receiver && serviceVariables.has(receiver)) return true;
|
|
2068
|
+
if (/^(srv|service|serviceClient|messaging|messageClient|eventClient)$/.test(candidate)) return true;
|
|
2069
|
+
return false;
|
|
2070
|
+
}
|
|
2071
|
+
function collectWrapperSpecs(source) {
|
|
2072
|
+
const specs = /* @__PURE__ */ new Map();
|
|
2073
|
+
const serviceVariables = collectServiceVariables(source);
|
|
2074
|
+
const calledNames = /* @__PURE__ */ new Set();
|
|
2075
|
+
const collectCalls = (node) => {
|
|
2076
|
+
if (ts7.isCallExpression(node)) {
|
|
2077
|
+
if (ts7.isIdentifier(node.expression)) calledNames.add(node.expression.text);
|
|
2078
|
+
if (ts7.isCallExpression(node.expression) && ts7.isIdentifier(node.expression.expression)) calledNames.add(node.expression.expression.text);
|
|
1992
2079
|
}
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
if (
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
if (ts5.isIdentifier(decl.name) && decl.initializer) {
|
|
2012
|
-
const sourceName = transactionReceiverName(decl.initializer);
|
|
2013
|
-
if (sourceName) cloneAliasBinding(decl.name.text, sourceName, "transaction", decl);
|
|
2080
|
+
ts7.forEachChild(node, collectCalls);
|
|
2081
|
+
};
|
|
2082
|
+
collectCalls(source);
|
|
2083
|
+
const scanFunction = (name, fn) => {
|
|
2084
|
+
if (!calledNames.has(name)) return;
|
|
2085
|
+
const params = fn.parameters.map((param) => ts7.isIdentifier(param.name) ? param.name.text : void 0);
|
|
2086
|
+
const sends = [];
|
|
2087
|
+
const visit = (node) => {
|
|
2088
|
+
if (ts7.isCallExpression(node) && ts7.isPropertyAccessExpression(node.expression) && node.expression.name.text === "send" && ts7.isIdentifier(node.expression.expression)) {
|
|
2089
|
+
const objectArg = node.arguments[0];
|
|
2090
|
+
if (objectArg && ts7.isObjectLiteralExpression(objectArg)) {
|
|
2091
|
+
const pathProp = propertyInitializer(objectArg, "path");
|
|
2092
|
+
const methodProp = propertyInitializer(objectArg, "method");
|
|
2093
|
+
const pathName = pathProp && ts7.isIdentifier(pathProp) ? pathProp.text : void 0;
|
|
2094
|
+
const methodName = methodProp && ts7.isIdentifier(methodProp) ? methodProp.text : void 0;
|
|
2095
|
+
const methodLiteral = resolveExpression(methodProp, node, "literal").value;
|
|
2096
|
+
if (pathName) sends.push({ client: node.expression.expression.text, path: pathName, method: methodName, methodLiteral, start: node.getStart(source), end: node.getEnd() });
|
|
2097
|
+
}
|
|
2014
2098
|
}
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2099
|
+
if (ts7.isCallExpression(node) && ts7.isIdentifier(node.expression) && specs.has(node.expression.text)) {
|
|
2100
|
+
const nested = specs.get(node.expression.text);
|
|
2101
|
+
const pathArg = nested ? node.arguments[nested.pathIndex] : void 0;
|
|
2102
|
+
const clientArg = nested?.clientIndex === void 0 ? void 0 : node.arguments[nested.clientIndex];
|
|
2103
|
+
const pathName = pathArg && ts7.isIdentifier(pathArg) ? pathArg.text : void 0;
|
|
2104
|
+
const clientName = clientArg && ts7.isIdentifier(clientArg) ? clientArg.text : nested?.clientName;
|
|
2105
|
+
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
|
+
}
|
|
2107
|
+
ts7.forEachChild(node, visit);
|
|
2108
|
+
};
|
|
2109
|
+
visit(fn);
|
|
2110
|
+
if (sends.length !== 1) return;
|
|
2111
|
+
const found = sends[0];
|
|
2112
|
+
const clientIndex = params.indexOf(found.client);
|
|
2113
|
+
const pathIndex = params.indexOf(found.path);
|
|
2114
|
+
const methodIndex = found.method ? params.indexOf(found.method) : -1;
|
|
2115
|
+
const capturesKnownClient = serviceVariables.has(found.client) || /^(srv|service|serviceClient|client|.*Client)$/.test(found.client);
|
|
2116
|
+
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
|
+
};
|
|
2118
|
+
const visitTop = (node) => {
|
|
2119
|
+
if (ts7.isFunctionDeclaration(node) && node.name) scanFunction(node.name.text, node);
|
|
2120
|
+
if (ts7.isVariableDeclaration(node) && ts7.isIdentifier(node.name) && node.initializer && (ts7.isArrowFunction(node.initializer) || ts7.isFunctionExpression(node.initializer))) scanFunction(node.name.text, node.initializer);
|
|
2121
|
+
ts7.forEachChild(node, visitTop);
|
|
2122
|
+
};
|
|
2123
|
+
visitTop(source);
|
|
2124
|
+
return specs;
|
|
2125
|
+
}
|
|
2126
|
+
function classifyOutboundCallsInSource(source, filePath) {
|
|
2127
|
+
const calls = [];
|
|
2128
|
+
const sourceFile = normalizePath(filePath);
|
|
2129
|
+
const initializers = variableInitializers(source);
|
|
2130
|
+
const serviceVariables = collectServiceVariables(source);
|
|
2131
|
+
const wrapperSpecs = collectWrapperSpecs(source);
|
|
2132
|
+
const wrapperInternalRanges = [...wrapperSpecs.values()].map((spec) => ({ start: spec.internalStart, end: spec.internalEnd }));
|
|
2133
|
+
const add = (node, fact, extra) => {
|
|
2134
|
+
calls.push({ node, fact: { ...fact, sourceFile, sourceLine: lineOf4(source.text, node.getStart(source)), confidence: fact.confidence ?? 0.8, evidence: parserEvidence(source, node, extra) } });
|
|
2135
|
+
};
|
|
2136
|
+
const visit = (node) => {
|
|
2137
|
+
if (ts7.isCallExpression(node)) {
|
|
2138
|
+
if (wrapperInternalRanges.some((range) => node.getStart(source) >= range.start && node.getEnd() <= range.end)) {
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
const expr = node.expression;
|
|
2142
|
+
const exprText = expr.getText(source);
|
|
2143
|
+
if (exprText === "cds.run") {
|
|
2144
|
+
const arg = node.arguments[0];
|
|
2145
|
+
const entity = arg ? queryEntityFromAst(arg, initializers) : void 0;
|
|
2146
|
+
const payload = arg?.getText(source) ?? "";
|
|
2147
|
+
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 (ts7.isPropertyAccessExpression(expr) && expr.name.text === "send" && (ts7.isIdentifier(expr.expression) || ts7.isPropertyAccessExpression(expr.expression))) {
|
|
2149
|
+
const objectArg = node.arguments[0];
|
|
2150
|
+
if (objectArg && ts7.isObjectLiteralExpression(objectArg)) {
|
|
2151
|
+
const receiver = receiverName(expr.expression);
|
|
2152
|
+
const query = objectPropertyText(objectArg, "query");
|
|
2153
|
+
const method = stripQuotes(resolveExpression(propertyInitializer(objectArg, "method"), node, "literal").value ?? objectPropertyText(objectArg, "method") ?? "POST");
|
|
2154
|
+
const pathExpr = propertyInitializer(objectArg, "path") ?? propertyInitializer(objectArg, "event");
|
|
2155
|
+
const resolvedPath = staticPathExpression(pathExpr, node);
|
|
2156
|
+
const op = pathExpr ? resolvedPath.text ?? pathExpr.getText(source) : void 0;
|
|
2157
|
+
const shorthandPath = objectPropertyIsShorthand(objectArg, "path");
|
|
2158
|
+
const candidateEvidence2 = pathExpr && ts7.isIdentifier(pathExpr) && !resolvedPath.text ? staticPathCandidates(pathExpr, node, method) : void 0;
|
|
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;
|
|
2161
|
+
const intent = classifyODataPathIntent(operationPathExpr, method);
|
|
2162
|
+
const entityCallTypes = { entity_mutation: "remote_entity_mutation", entity_delete: "remote_entity_delete", entity_media: "remote_entity_media", entity_candidate: "remote_entity_candidate" };
|
|
2163
|
+
const entityCallType = entityCallTypes[intent.kind];
|
|
2164
|
+
const isODataQueryRead = method.toUpperCase() === "GET" && ["entity_query", "entity_key_read", "entity_navigation_query"].includes(intent.kind);
|
|
2165
|
+
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: !query && pathExpr && !operationPathExpr ? "dynamic_operation_path_identifier" : void 0 }, { receiver, classifier: "service_client_send_object", operationPathExpression: shorthandPath ? op : void 0, rawPathExpression: resolvedPath.rawExpression, literalPathSource: resolvedPath.text ? resolvedPath.sourceKind === "const" ? "same_scope_const_initializer" : resolvedPath.sourceKind : void 0, odataPathIntent: operationPathExpr ? intent : void 0, staticPathCandidates: candidateEvidence2, parserWarning: !query && pathExpr && !operationPathExpr ? "dynamic_operation_path_identifier" : void 0 });
|
|
2166
|
+
} else {
|
|
2167
|
+
const receiver = receiverName(expr.expression);
|
|
2168
|
+
const rootReceiver = rootReceiverName(expr.expression);
|
|
2169
|
+
const firstArg2 = resolveExpression(node.arguments[0], node, "literal");
|
|
2170
|
+
const method = firstArg2.value?.toUpperCase();
|
|
2171
|
+
const pathArg = node.arguments[1];
|
|
2172
|
+
const supported = method && supportedHttpMethods.has(method);
|
|
2173
|
+
if (receiver && supported && serviceVariables.has(rootReceiver ?? receiver)) {
|
|
2174
|
+
const resolvedPath = staticPathExpression(pathArg, node);
|
|
2175
|
+
const operationPathExpr = operationPathFromStatic(resolvedPath.text);
|
|
2176
|
+
const intent = classifyODataPathIntent(operationPathExpr, method);
|
|
2177
|
+
add(node, { callType: "remote_action", serviceVariableName: rootReceiver ?? receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: operationPathExpr ? 0.8 : 0.45, unresolvedReason: operationPathExpr ? void 0 : "dynamic_operation_path_identifier" }, { receiver, rootReceiver, classifier: "service_client_send_method_path", rawPathExpression: resolvedPath.rawExpression, literalPathSource: resolvedPath.text ? resolvedPath.sourceKind === "const" ? "same_scope_const_initializer" : resolvedPath.sourceKind : void 0, odataPathIntent: operationPathExpr ? intent : void 0, parserWarning: operationPathExpr ? void 0 : "dynamic_operation_path_identifier" });
|
|
2178
|
+
} else if (receiver && serviceVariables.has(rootReceiver ?? receiver)) {
|
|
2179
|
+
const operationPathExpr = safeOperationName(firstArg2.value);
|
|
2180
|
+
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
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
} else if (ts7.isCallExpression(expr) && ts7.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text) || ts7.isIdentifier(expr) && wrapperSpecs.has(expr.text)) {
|
|
2184
|
+
const wrapperName = ts7.isIdentifier(expr) ? expr.text : ts7.isCallExpression(expr) && ts7.isIdentifier(expr.expression) ? expr.expression.text : "";
|
|
2185
|
+
const wrapperArgs = ts7.isIdentifier(expr) ? node.arguments : ts7.isCallExpression(expr) ? expr.arguments : node.arguments;
|
|
2186
|
+
const spec = wrapperSpecs.get(wrapperName);
|
|
2187
|
+
const clientArg = spec?.clientIndex === void 0 ? void 0 : wrapperArgs[spec.clientIndex];
|
|
2188
|
+
const pathArg = spec ? wrapperArgs[spec.pathIndex] : void 0;
|
|
2189
|
+
const methodArg = spec?.methodIndex === void 0 ? void 0 : wrapperArgs[spec.methodIndex];
|
|
2190
|
+
const receiver = clientArg && ts7.isIdentifier(clientArg) ? clientArg.text : spec?.clientName;
|
|
2191
|
+
const method = stripQuotes(resolveExpression(methodArg, node, "literal").value ?? spec?.methodLiteral ?? "POST");
|
|
2192
|
+
const resolvedPath = staticPathExpression(pathArg, node);
|
|
2193
|
+
const operationPathExpr = operationPathFromStatic(resolvedPath.text);
|
|
2194
|
+
const normalizedOperationPath = operationPathExpr ? classifyODataPathIntent(operationPathExpr, method).topLevelOperationName : void 0;
|
|
2195
|
+
if (spec && receiver && operationPathExpr) {
|
|
2196
|
+
add(node, { callType: "remote_action", serviceVariableName: receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.75 }, { receiver, classifier: resolvedPath.sourceKind === "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: resolvedPath.sourceKind, rawPathExpression: resolvedPath.rawExpression, normalizedOperationPath, literalPathSource: resolvedPath.sourceKind === "const" ? "same_scope_const_initializer" : `wrapper_call_${resolvedPath.sourceKind}`, literalCallerArgumentDetected: true });
|
|
2197
|
+
} else if (spec && receiver) {
|
|
2198
|
+
add(node, { callType: "remote_action", serviceVariableName: receiver, method, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.45, unresolvedReason: "dynamic_operation_path_identifier" }, { receiver, classifier: "higher_order_wrapper_dynamic_path", wrapperFunction: wrapperName, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf4(source.text, node.getStart(source)), wrapperPathSourceKind: resolvedPath.sourceKind, rawPathExpression: resolvedPath.rawExpression, parserWarning: "dynamic_operation_path_identifier" });
|
|
2199
|
+
}
|
|
2200
|
+
} else if (ts7.isPropertyAccessExpression(expr) && ["emit", "publish", "on"].includes(expr.name.text)) {
|
|
2201
|
+
const receiver = receiverName(expr.expression);
|
|
2202
|
+
const rootReceiver = rootReceiverName(expr.expression);
|
|
2203
|
+
if (isSupportedEventReceiver(receiver, rootReceiver, serviceVariables)) {
|
|
2204
|
+
const eventName = literalText(node.arguments[0]);
|
|
2205
|
+
if (eventName) add(node, { callType: expr.name.text === "on" ? "async_subscribe" : "async_emit", serviceVariableName: rootReceiver ?? receiver, eventNameExpr: eventName }, { receiver, rootReceiver, classifier: expr.name.text === "on" ? "cap_service_event_subscription" : "cap_service_event_emit", receiverClassification: "cap_evidence" });
|
|
2206
|
+
}
|
|
2207
|
+
} else {
|
|
2208
|
+
const external = externalHttpEvidence(node, source);
|
|
2209
|
+
if (external) {
|
|
2210
|
+
const evidenceTarget = { ...external.externalTarget, method: external.method, parserClassifier: external.classifier, sourceCallShape: external.sourceCallShape };
|
|
2211
|
+
const safeTarget = externalHttpTarget({ method: external.method, evidence_json: JSON.stringify({ externalTarget: evidenceTarget }) });
|
|
2212
|
+
add(node, { callType: "external_http", method: external.method, payloadSummary: void 0, confidence: 0.7, unresolvedReason: "External HTTP destination is outside indexed CAP services", externalTarget: { kind: safeTarget.kind, stableId: safeTarget.toId, label: safeTarget.label, dynamic: safeTarget.dynamic } }, { classifier: external.classifier, externalTarget: safeTarget, sourceCallShape: external.sourceCallShape });
|
|
2213
|
+
}
|
|
2023
2214
|
}
|
|
2024
|
-
await recordBindingFromExpression(assignment.left.text, assignment.right, assignment, "assignment");
|
|
2025
|
-
continue;
|
|
2026
2215
|
}
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
await recordArrayDestructuredAssignment(left, assignment.right, assignment);
|
|
2032
|
-
}
|
|
2033
|
-
return out;
|
|
2216
|
+
ts7.forEachChild(node, visit);
|
|
2217
|
+
};
|
|
2218
|
+
visit(source);
|
|
2219
|
+
return calls;
|
|
2034
2220
|
}
|
|
2035
|
-
function
|
|
2036
|
-
const
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
const className = stmt.name.text;
|
|
2080
|
-
const helperName = member.name.text;
|
|
2081
|
-
const initializer = member.initializer;
|
|
2082
|
-
if (!ts5.isArrowFunction(initializer) && !ts5.isFunctionExpression(initializer))
|
|
2083
|
-
continue;
|
|
2084
|
-
const bindings = /* @__PURE__ */ new Map();
|
|
2085
|
-
visit2(initializer);
|
|
2221
|
+
function containsSupportedOutboundCall(node) {
|
|
2222
|
+
const source = node.getSourceFile();
|
|
2223
|
+
const start = node.getFullStart();
|
|
2224
|
+
const end = node.getEnd();
|
|
2225
|
+
return classifyOutboundCallsInSource(source, source.fileName).some((call) => call.node.getStart(source) >= start && call.node.getEnd() <= end);
|
|
2226
|
+
}
|
|
2227
|
+
async function parseOutboundCalls(repoPath, filePath) {
|
|
2228
|
+
const text = await fs7.readFile(path10.join(repoPath, filePath), "utf8");
|
|
2229
|
+
const source = ts7.createSourceFile(filePath, text, ts7.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts7.ScriptKind.TS : ts7.ScriptKind.JS);
|
|
2230
|
+
const bindingNames = new Set((await parseServiceBindings(repoPath, filePath)).map((binding) => binding.variableName));
|
|
2231
|
+
const importedWrappers = await parseImportedWrapperCalls(repoPath, filePath, source, bindingNames);
|
|
2232
|
+
return [...classifyOutboundCallsInSource(source, filePath).map((call) => call.fact), ...importedWrappers, ...parseLocalServiceCalls(text, filePath)];
|
|
2233
|
+
}
|
|
2234
|
+
function parseLocalServiceCalls(text, filePath) {
|
|
2235
|
+
const source = ts7.createSourceFile(filePath, text, ts7.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts7.ScriptKind.TS : ts7.ScriptKind.JS);
|
|
2236
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
2237
|
+
const calls = [];
|
|
2238
|
+
const visit = (node) => {
|
|
2239
|
+
if (ts7.isVariableDeclaration(node) && ts7.isIdentifier(node.name) && node.initializer) {
|
|
2240
|
+
const origin = serviceLookup(node.initializer, aliases);
|
|
2241
|
+
if (origin) aliases.set(node.name.text, { ...origin, chain: [...origin.chain, node.name.text] });
|
|
2242
|
+
}
|
|
2243
|
+
if (ts7.isCallExpression(node)) {
|
|
2244
|
+
const parsed = serviceOperationCall(node, aliases);
|
|
2245
|
+
if (parsed && parsed.operation !== "entities") calls.push({
|
|
2246
|
+
callType: "local_service_call",
|
|
2247
|
+
operationPathExpr: `/${parsed.operation}`,
|
|
2248
|
+
payloadSummary: parsed.service,
|
|
2249
|
+
localServiceName: parsed.service,
|
|
2250
|
+
localServiceLookup: parsed.lookup,
|
|
2251
|
+
aliasChain: parsed.chain,
|
|
2252
|
+
sourceFile: normalizePath(filePath),
|
|
2253
|
+
sourceLine: lineOf4(text, node.getStart(source)),
|
|
2254
|
+
confidence: 0.9,
|
|
2255
|
+
unresolvedReason: ["send", "emit", "publish", "on"].includes(parsed.operation) ? "transport_client_method" : void 0,
|
|
2256
|
+
evidence: parserEvidence(source, node, {
|
|
2257
|
+
classifier: parsed.classifier,
|
|
2258
|
+
parserCallType: parsed.operation === "send" ? "transport_client_method" : parsed.classifier,
|
|
2259
|
+
localServiceLookup: parsed.lookup,
|
|
2260
|
+
localServiceName: parsed.service,
|
|
2261
|
+
operation: parsed.operation,
|
|
2262
|
+
aliasChain: parsed.chain
|
|
2263
|
+
})
|
|
2264
|
+
});
|
|
2086
2265
|
}
|
|
2266
|
+
ts7.forEachChild(node, visit);
|
|
2267
|
+
};
|
|
2268
|
+
visit(source);
|
|
2269
|
+
return calls;
|
|
2270
|
+
}
|
|
2271
|
+
function serviceLookup(expr, aliases) {
|
|
2272
|
+
if (ts7.isIdentifier(expr)) return aliases.get(expr.text);
|
|
2273
|
+
if (ts7.isPropertyAccessExpression(expr) && expr.expression.getText() === "cds.services") return { service: expr.name.text, lookup: expr.getText(), chain: [expr.getText()] };
|
|
2274
|
+
if (ts7.isElementAccessExpression(expr) && expr.expression.getText() === "cds.services" && ts7.isStringLiteral(expr.argumentExpression)) return { service: expr.argumentExpression.text, lookup: expr.getText(), chain: [expr.getText()] };
|
|
2275
|
+
return void 0;
|
|
2276
|
+
}
|
|
2277
|
+
function serviceOperationCall(node, aliases) {
|
|
2278
|
+
const expr = node.expression;
|
|
2279
|
+
if (!ts7.isPropertyAccessExpression(expr)) return void 0;
|
|
2280
|
+
const origin = serviceLookup(expr.expression, aliases);
|
|
2281
|
+
if (!origin) return void 0;
|
|
2282
|
+
if (expr.name.text === "send") {
|
|
2283
|
+
const first = literalText(node.arguments[0]);
|
|
2284
|
+
const second = literalText(node.arguments[1]);
|
|
2285
|
+
const method = first?.toUpperCase();
|
|
2286
|
+
if (method && ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"].includes(method) && second) return { ...origin, operation: second.replace(/^\//, ""), classifier: "cap_service_send_method_path" };
|
|
2287
|
+
if (first) return { ...origin, operation: first.replace(/^\//, ""), classifier: "cap_service_send_local_dispatch" };
|
|
2087
2288
|
}
|
|
2088
|
-
return
|
|
2289
|
+
return { ...origin, operation: expr.name.text, classifier: "local_cap_service_call" };
|
|
2089
2290
|
}
|
|
2090
2291
|
|
|
2091
2292
|
// src/linker/dynamic-edge-resolver.ts
|
|
@@ -2361,9 +2562,9 @@ function generatedFromConstantName(value) {
|
|
|
2361
2562
|
return void 0;
|
|
2362
2563
|
}
|
|
2363
2564
|
function resolved(value, raw) {
|
|
2364
|
-
const
|
|
2365
|
-
const generated = generatedFromConstantName(
|
|
2366
|
-
return { status: "resolved", operationName: generated ?? normalizedOperationName(
|
|
2565
|
+
const literal2 = clean(value);
|
|
2566
|
+
const generated = generatedFromConstantName(literal2);
|
|
2567
|
+
return { status: "resolved", operationName: generated ?? normalizedOperationName(literal2), raw };
|
|
2367
2568
|
}
|
|
2368
2569
|
function normalizeDecoratorOperationSignal(value, raw, candidateOperation) {
|
|
2369
2570
|
if (value) return resolved(value, raw);
|
|
@@ -2509,7 +2710,25 @@ function objectJson(value) {
|
|
|
2509
2710
|
}
|
|
2510
2711
|
function callEvidence(call, resolution, servicePath, op, destination, normalized, odataPathIntent) {
|
|
2511
2712
|
const bindingHasDynamicExpression = Boolean(Number(call.isDynamic ?? 0));
|
|
2512
|
-
|
|
2713
|
+
const routingPlaceholderKeys = placeholderKeys([servicePath, destination, stringValue2(call.aliasExpr), stringValue2(call.alias)]);
|
|
2714
|
+
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 };
|
|
2715
|
+
}
|
|
2716
|
+
function compactCandidateScores(candidates) {
|
|
2717
|
+
return candidates.flatMap((candidate) => {
|
|
2718
|
+
const row = objectValue(candidate);
|
|
2719
|
+
if (!row) return [];
|
|
2720
|
+
return [{ repo: row.repoName, servicePath: row.servicePath, operationPath: row.operationPath, score: row.score, reasons: row.reasons }];
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2723
|
+
function placeholderKeys(values) {
|
|
2724
|
+
const keys = values.flatMap((value) => [...(value ?? "").matchAll(/\$\{([^}]*)\}/g)].map((match) => (match[1] ?? "").trim()).filter(Boolean));
|
|
2725
|
+
return [...new Set(keys)].sort();
|
|
2726
|
+
}
|
|
2727
|
+
function objectValue(value) {
|
|
2728
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2729
|
+
}
|
|
2730
|
+
function stringValue2(value) {
|
|
2731
|
+
return typeof value === "string" ? value : void 0;
|
|
2513
2732
|
}
|
|
2514
2733
|
function linkImplementations(db, workspaceId, generation) {
|
|
2515
2734
|
const operations = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,o.operation_name operationName,o.provenance provenance,o.base_operation_id baseOperationId,s.service_path servicePath,s.repo_id modelRepoId,r.name modelRepo,r.package_name modelPackage,r.kind modelKind 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=?`).all(workspaceId);
|
|
@@ -2524,7 +2743,11 @@ function linkImplementations(db, workspaceId, generation) {
|
|
|
2524
2743
|
const accepted = candidates.filter((candidate) => candidate.accepted);
|
|
2525
2744
|
const topScore = accepted[0]?.score ?? 0;
|
|
2526
2745
|
const winners = accepted.filter((candidate) => candidate.score === topScore);
|
|
2527
|
-
const
|
|
2746
|
+
const duplicateFamilies = duplicatePackageFamilies(accepted);
|
|
2747
|
+
const duplicatePackageAmbiguous = duplicateFamilies.length > 0 && !accepted.some(hasDirectOwnershipEvidence);
|
|
2748
|
+
const selected = duplicatePackageAmbiguous ? accepted : winners;
|
|
2749
|
+
const unique = !duplicatePackageAmbiguous && winners.length === 1 ? winners[0] : void 0;
|
|
2750
|
+
const ambiguityReasons = duplicatePackageAmbiguous ? ["duplicate_package_name_candidates"] : winners.length > 1 ? ["multiple_equal_score_implementation_candidates"] : [];
|
|
2528
2751
|
const evidence = {
|
|
2529
2752
|
servicePath: operation.servicePath,
|
|
2530
2753
|
operationPath: operation.operationPath,
|
|
@@ -2533,6 +2756,8 @@ function linkImplementations(db, workspaceId, generation) {
|
|
|
2533
2756
|
implementationSource: implementationContext.operationId === operation.operationId ? "direct_or_concrete_override" : "inherited_from_base_operation",
|
|
2534
2757
|
baseOperationId: operation.baseOperationId,
|
|
2535
2758
|
implementationOperationId: implementationContext.operationId,
|
|
2759
|
+
ambiguityReasons,
|
|
2760
|
+
candidateFamilies: duplicateFamilies,
|
|
2536
2761
|
candidates: candidates.map((candidate, index) => candidateEvidence(candidate, index + 1))
|
|
2537
2762
|
};
|
|
2538
2763
|
if (accepted.length === 0) {
|
|
@@ -2541,7 +2766,7 @@ function linkImplementations(db, workspaceId, generation) {
|
|
|
2541
2766
|
unresolvedCount += 1;
|
|
2542
2767
|
continue;
|
|
2543
2768
|
}
|
|
2544
|
-
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", unique ? "resolved" : "ambiguous", "operation", graphId(operation.operationId), unique ? "handler_method" : "handler_method_candidates", unique ? graphId(unique.methodId) :
|
|
2769
|
+
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", unique ? "resolved" : "ambiguous", "operation", graphId(operation.operationId), unique ? "handler_method" : "handler_method_candidates", unique ? graphId(unique.methodId) : selected.map((row) => graphId(row.methodId)).join(","), unique ? 0.95 : 0.5, JSON.stringify(evidence), 0, unique ? null : "Ambiguous registered handler implementation candidates", generation);
|
|
2545
2770
|
edgeCount += 1;
|
|
2546
2771
|
if (unique) resolvedCount += 1;
|
|
2547
2772
|
else ambiguousCount += 1;
|
|
@@ -2585,6 +2810,18 @@ function uniqueRegistrations(rows2) {
|
|
|
2585
2810
|
return true;
|
|
2586
2811
|
});
|
|
2587
2812
|
}
|
|
2813
|
+
function duplicatePackageFamilies(candidates) {
|
|
2814
|
+
const byPackage = /* @__PURE__ */ new Map();
|
|
2815
|
+
for (const candidate of candidates) {
|
|
2816
|
+
const packageName = typeof candidate.handlerPackage === "string" ? candidate.handlerPackage : void 0;
|
|
2817
|
+
if (!packageName) continue;
|
|
2818
|
+
byPackage.set(packageName, [...byPackage.get(packageName) ?? [], candidate]);
|
|
2819
|
+
}
|
|
2820
|
+
return [...byPackage.entries()].filter(([, rows2]) => new Set(rows2.map((row) => Number(row.handlerRepoId))).size > 1).map(([packageName, rows2]) => ({ reason: "duplicate_package_name_candidates", packageName, count: rows2.length, repositories: rows2.map((row) => row.handlerRepo).sort() }));
|
|
2821
|
+
}
|
|
2822
|
+
function hasDirectOwnershipEvidence(candidate) {
|
|
2823
|
+
return candidate.acceptedReasons.some((reason) => reason === "model package equals registration package" || reason === "model package equals handler package" || reason === "registration package contains exact local service path");
|
|
2824
|
+
}
|
|
2588
2825
|
function implementationCandidates(db, workspaceId, operation) {
|
|
2589
2826
|
const modelRepoGraphId = graphId(operation.modelRepoId);
|
|
2590
2827
|
return db.prepare(`SELECT DISTINCT
|
|
@@ -2768,6 +3005,262 @@ function normalizedOperation(value) {
|
|
|
2768
3005
|
return value.startsWith("/") ? value.slice(1) : value;
|
|
2769
3006
|
}
|
|
2770
3007
|
|
|
3008
|
+
// src/trace/implementation-hints.ts
|
|
3009
|
+
function parseImplementationHint(value) {
|
|
3010
|
+
const hint = {};
|
|
3011
|
+
for (const part of value.split(",")) {
|
|
3012
|
+
const separator = part.indexOf("=");
|
|
3013
|
+
if (separator <= 0 || separator === part.length - 1) throw new Error(`Invalid implementation hint field: ${part}`);
|
|
3014
|
+
assignHintField(hint, part.slice(0, separator).trim(), part.slice(separator + 1).trim());
|
|
3015
|
+
}
|
|
3016
|
+
if (!hint.implementationRepo) throw new Error("Scoped implementation hint requires an implementation repo selection");
|
|
3017
|
+
return { ...hint, implementationRepo: hint.implementationRepo };
|
|
3018
|
+
}
|
|
3019
|
+
function selectImplementation(rawEvidence, hints, legacyRepo) {
|
|
3020
|
+
const evidence = asEvidence(rawEvidence);
|
|
3021
|
+
const scoped = hints ?? [];
|
|
3022
|
+
const matchingHints = scoped.filter((hint2) => hintMatchesEdge(hint2, evidence));
|
|
3023
|
+
if (matchingHints.length === 0) {
|
|
3024
|
+
if (legacyRepo) return selectCandidate(evidence, legacyHint(legacyRepo), "implementation_repo_hint");
|
|
3025
|
+
const reason = scoped.length > 0 ? "no_scoped_hint_matched_edge" : "no_implementation_hint_supplied";
|
|
3026
|
+
return { blocksAutomatic: false, evidence: { status: "not_matched", reason, strategy: "scoped_implementation_hint" } };
|
|
3027
|
+
}
|
|
3028
|
+
if (matchingHints.length > 1) {
|
|
3029
|
+
return {
|
|
3030
|
+
blocksAutomatic: true,
|
|
3031
|
+
evidence: {
|
|
3032
|
+
status: "tied",
|
|
3033
|
+
reason: "multiple_scoped_hints_matched_edge",
|
|
3034
|
+
strategy: "scoped_implementation_hint",
|
|
3035
|
+
matchedHints: matchingHints,
|
|
3036
|
+
candidateCount: matchingHints.length
|
|
3037
|
+
}
|
|
3038
|
+
};
|
|
3039
|
+
}
|
|
3040
|
+
const hint = matchingHints[0];
|
|
3041
|
+
return hint ? selectCandidate(evidence, hint, "scoped_implementation_hint") : { blocksAutomatic: false, evidence: { status: "not_matched" } };
|
|
3042
|
+
}
|
|
3043
|
+
function implementationHintDiagnostic(selection) {
|
|
3044
|
+
if (!selection.blocksAutomatic || selection.methodId) return void 0;
|
|
3045
|
+
return {
|
|
3046
|
+
severity: "warning",
|
|
3047
|
+
code: "implementation_hint_mismatch",
|
|
3048
|
+
message: "Implementation hint did not select exactly one viable candidate",
|
|
3049
|
+
hintStatus: selection.evidence.status,
|
|
3050
|
+
candidateCount: selection.evidence.candidateCount,
|
|
3051
|
+
implementationSelection: selection.evidence
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
function assignHintField(hint, key, value) {
|
|
3055
|
+
if (key === "service" || key === "servicePath") hint.servicePath = value;
|
|
3056
|
+
else if (key === "operation" || key === "operationPath") hint.operationPath = value;
|
|
3057
|
+
else if (key === "package" || key === "packageName") hint.packageName = value;
|
|
3058
|
+
else if (key === "repository" || key === "repositoryName") hint.repositoryName = value;
|
|
3059
|
+
else if (key === "family" || key === "candidateFamily") hint.candidateFamily = value;
|
|
3060
|
+
else if (key === "repo" || key === "implementationRepo" || key === "select") hint.implementationRepo = value;
|
|
3061
|
+
else throw new Error(`Unknown implementation hint field: ${key}`);
|
|
3062
|
+
}
|
|
3063
|
+
function selectCandidate(evidence, hint, strategy) {
|
|
3064
|
+
const matches2 = (evidence.candidates ?? []).filter((candidate) => candidate.accepted && candidateMatchesRepo(candidate, hint.implementationRepo));
|
|
3065
|
+
const selected = matches2.length === 1 ? matches2[0] : void 0;
|
|
3066
|
+
if (!selected || selected.methodId === void 0) {
|
|
3067
|
+
return {
|
|
3068
|
+
blocksAutomatic: true,
|
|
3069
|
+
evidence: {
|
|
3070
|
+
status: matches2.length > 1 ? "tied" : "not_matched",
|
|
3071
|
+
reason: matches2.length > 1 ? "hint_matched_multiple_candidates" : "hint_matched_zero_candidates",
|
|
3072
|
+
strategy,
|
|
3073
|
+
matchedHint: hint,
|
|
3074
|
+
selectedRepo: hint.implementationRepo,
|
|
3075
|
+
candidateCount: matches2.length
|
|
3076
|
+
}
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
3079
|
+
return {
|
|
3080
|
+
methodId: String(selected.methodId),
|
|
3081
|
+
blocksAutomatic: false,
|
|
3082
|
+
evidence: {
|
|
3083
|
+
status: "selected",
|
|
3084
|
+
guided: true,
|
|
3085
|
+
strategy,
|
|
3086
|
+
matchedHint: hint,
|
|
3087
|
+
selectedRepo: hint.implementationRepo,
|
|
3088
|
+
selectedMethodId: selected.methodId,
|
|
3089
|
+
ambiguityReason: evidence.ambiguityReasons?.[0]
|
|
3090
|
+
}
|
|
3091
|
+
};
|
|
3092
|
+
}
|
|
3093
|
+
function hintMatchesEdge(hint, evidence) {
|
|
3094
|
+
const model = evidence.modelPackage ?? evidence.candidates?.[0]?.modelPackage;
|
|
3095
|
+
const familyNames = /* @__PURE__ */ new Set([
|
|
3096
|
+
...(evidence.candidateFamilies ?? []).flatMap((family) => family.packageName ? [family.packageName] : []),
|
|
3097
|
+
...(evidence.candidates ?? []).flatMap((candidate) => candidate.handlerPackage?.packageName ? [candidate.handlerPackage.packageName] : [])
|
|
3098
|
+
]);
|
|
3099
|
+
return matches(hint.servicePath, evidence.servicePath ?? evidence.candidates?.[0]?.servicePath) && matches(hint.operationPath, evidence.operationPath ?? evidence.candidates?.[0]?.operationPath) && matches(hint.packageName, model?.packageName) && matches(hint.repositoryName, model?.name) && (!hint.candidateFamily || familyNames.has(hint.candidateFamily));
|
|
3100
|
+
}
|
|
3101
|
+
function candidateMatchesRepo(candidate, value) {
|
|
3102
|
+
return candidate.handlerPackage?.name === value || candidate.handlerPackage?.packageName === value || candidate.sourceFile?.startsWith(value) === true;
|
|
3103
|
+
}
|
|
3104
|
+
function matches(expected, actual) {
|
|
3105
|
+
return expected === void 0 || expected === actual;
|
|
3106
|
+
}
|
|
3107
|
+
function legacyHint(implementationRepo) {
|
|
3108
|
+
return { implementationRepo };
|
|
3109
|
+
}
|
|
3110
|
+
function asEvidence(value) {
|
|
3111
|
+
return value;
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
// src/trace/evidence.ts
|
|
3115
|
+
function baseTraceEvidence(row, call, persistedEvidence, contextualEvidence) {
|
|
3116
|
+
const evidence = { ...persistedEvidence, ...contextualEvidence ?? {} };
|
|
3117
|
+
return {
|
|
3118
|
+
...evidence,
|
|
3119
|
+
graphEdgeId: row.id,
|
|
3120
|
+
persistedGraphEdgeId: row.id > 0 ? row.id : void 0,
|
|
3121
|
+
outboundCallId: call.id,
|
|
3122
|
+
callSite: { sourceFile: call.source_file, sourceLine: call.source_line },
|
|
3123
|
+
sourceFile: call.source_file,
|
|
3124
|
+
sourceLine: call.source_line,
|
|
3125
|
+
file: call.source_file,
|
|
3126
|
+
line: call.source_line,
|
|
3127
|
+
persistedTarget: { kind: row.to_kind, id: row.to_id },
|
|
3128
|
+
contextualResolutionParticipated: Boolean(contextualEvidence?.contextualServiceBindingAttempted),
|
|
3129
|
+
persistedResolution: persistedResolution(row)
|
|
3130
|
+
};
|
|
3131
|
+
}
|
|
3132
|
+
function runtimeResolution(db, row, evidence, vars, workspaceId) {
|
|
3133
|
+
const substituted = evidenceWithRuntimeVariables(evidence, vars);
|
|
3134
|
+
if (!isRemoteRuntimeCandidate(row, evidence, vars)) {
|
|
3135
|
+
const withSections = withEffectiveResolution(substituted, row, row.unresolved_reason);
|
|
3136
|
+
return { row, evidence: withSections, unresolvedReason: row.unresolved_reason };
|
|
3137
|
+
}
|
|
3138
|
+
const resolution = resolveRuntimeOperation(db, substituted, workspaceId);
|
|
3139
|
+
if (resolution.target) {
|
|
3140
|
+
const resolvedRow = { ...row, status: "resolved", to_kind: "operation", to_id: String(resolution.target.operationId), unresolved_reason: void 0, confidence: Math.max(0, Math.min(1, resolution.target.score)) };
|
|
3141
|
+
return { row: resolvedRow, evidence: withEffectiveResolution(substituted, resolvedRow, void 0, resolution), target: resolution.target };
|
|
3142
|
+
}
|
|
3143
|
+
const unresolvedReason = runtimeUnresolvedReason(resolution);
|
|
3144
|
+
return { row, evidence: withEffectiveResolution(substituted, row, unresolvedReason, resolution), unresolvedReason };
|
|
3145
|
+
}
|
|
3146
|
+
function runtimeVariableDiagnostic(edges) {
|
|
3147
|
+
const missing = /* @__PURE__ */ new Set();
|
|
3148
|
+
for (const edge of edges) {
|
|
3149
|
+
const substitutions = edge.evidence.runtimeSubstitutions;
|
|
3150
|
+
if (!substitutions || typeof substitutions !== "object" || Array.isArray(substitutions)) continue;
|
|
3151
|
+
for (const value of Object.values(substitutions))
|
|
3152
|
+
for (const key of value.missing ?? []) missing.add(key);
|
|
3153
|
+
}
|
|
3154
|
+
const missingVariables = [...missing].sort();
|
|
3155
|
+
if (missingVariables.length === 0) return void 0;
|
|
3156
|
+
return {
|
|
3157
|
+
severity: "warning",
|
|
3158
|
+
code: "trace_runtime_variables_missing",
|
|
3159
|
+
message: `Runtime variables are required to resolve dynamic trace targets: ${missingVariables.join(", ")}`,
|
|
3160
|
+
missingVariables,
|
|
3161
|
+
suggestions: missingVariables.map((key) => `--var ${key}=<value>`)
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
function edgeTarget(row, evidence) {
|
|
3165
|
+
const effective = parseObject(evidence.effectiveResolution);
|
|
3166
|
+
const targetServicePath = stringValue3(effective.targetServicePath ?? evidence.targetServicePath);
|
|
3167
|
+
const targetOperationPath = stringValue3(effective.targetOperationPath ?? evidence.targetOperationPath);
|
|
3168
|
+
if (targetServicePath && targetOperationPath) return `${targetServicePath}${targetOperationPath}`;
|
|
3169
|
+
const runtimeCandidate = evidence.runtimeResolvedCandidate;
|
|
3170
|
+
if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath) return `${runtimeCandidate.servicePath}${runtimeCandidate.operationPath}`;
|
|
3171
|
+
const servicePath = stringValue3(evidence.servicePath);
|
|
3172
|
+
const operationPath = stringValue3(evidence.operationPath);
|
|
3173
|
+
const targetOperation = stringValue3(evidence.targetOperation);
|
|
3174
|
+
const targetRepo = stringValue3(evidence.targetRepo) ?? "";
|
|
3175
|
+
if (row.edge_type === "HANDLER_RUNS_DB_QUERY") return `Entity: ${row.to_id || "unknown"}`;
|
|
3176
|
+
if (row.edge_type === "HANDLER_RUNS_REMOTE_QUERY") return stringValue3(evidence.remoteQueryTarget) ?? `Remote query: ${row.to_id || "unknown"}`;
|
|
3177
|
+
if (row.edge_type === "HANDLER_CALLS_EXTERNAL_HTTP") {
|
|
3178
|
+
const target = parseObject(evidence.externalTarget);
|
|
3179
|
+
return stringValue3(target.label) ?? `External endpoint: ${row.to_id || "unknown"}`;
|
|
3180
|
+
}
|
|
3181
|
+
if (servicePath && operationPath) return `${servicePath}${operationPath}`;
|
|
3182
|
+
return targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
|
|
3183
|
+
}
|
|
3184
|
+
function persistedResolution(row) {
|
|
3185
|
+
return {
|
|
3186
|
+
status: row.status,
|
|
3187
|
+
targetKind: row.to_kind,
|
|
3188
|
+
targetId: row.to_id,
|
|
3189
|
+
edgeId: row.id > 0 ? row.id : void 0,
|
|
3190
|
+
confidence: row.confidence,
|
|
3191
|
+
unresolvedReason: row.unresolved_reason,
|
|
3192
|
+
edgeType: row.edge_type
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
3195
|
+
function effectiveResolution(row, evidence, unresolvedReason, resolution) {
|
|
3196
|
+
const target = resolution?.target;
|
|
3197
|
+
return {
|
|
3198
|
+
status: target ? "resolved" : row.status,
|
|
3199
|
+
targetKind: target ? "operation" : row.to_kind,
|
|
3200
|
+
targetId: target ? String(target.operationId) : row.to_id,
|
|
3201
|
+
targetRepo: target?.repoName ?? evidence.targetRepo,
|
|
3202
|
+
targetServicePath: target?.servicePath ?? evidence.targetServicePath,
|
|
3203
|
+
targetOperationPath: target?.operationPath ?? evidence.targetOperationPath,
|
|
3204
|
+
targetOperation: target?.operationName ?? evidence.targetOperation,
|
|
3205
|
+
confidence: target?.score ?? row.confidence,
|
|
3206
|
+
reasons: resolution?.reasons ?? evidence.resolutionReasons,
|
|
3207
|
+
unresolvedReason,
|
|
3208
|
+
edgeType: target ? "REMOTE_CALL_RESOLVES_TO_OPERATION" : row.edge_type
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
function withEffectiveResolution(evidence, row, unresolvedReason, resolution) {
|
|
3212
|
+
const current = effectiveResolution(row, evidence, unresolvedReason, resolution);
|
|
3213
|
+
const rest = { ...evidence };
|
|
3214
|
+
delete rest.runtimeResolvedCandidate;
|
|
3215
|
+
return { ...rest, effectiveResolution: current, linker: { status: current.status, confidence: current.confidence, reason: unresolvedReason, edgeType: current.edgeType } };
|
|
3216
|
+
}
|
|
3217
|
+
function resolveRuntimeOperation(db, evidence, workspaceId) {
|
|
3218
|
+
const servicePath = stringValue3(evidence.servicePath);
|
|
3219
|
+
const rawOperationPath = stringValue3(evidence.operationPath);
|
|
3220
|
+
const normalized = normalizeODataOperationInvocationPath(rawOperationPath);
|
|
3221
|
+
const operationPath = normalized?.wasInvocation ? normalized.normalizedOperationPath : stringValue3(evidence.normalizedOperationPath) ?? rawOperationPath;
|
|
3222
|
+
const alias = stringValue3(evidence.serviceAliasExpr ?? evidence.serviceAlias);
|
|
3223
|
+
const destination = stringValue3(evidence.destination);
|
|
3224
|
+
return resolveOperation(db, { servicePath, operationPath, alias, destination, hasExplicitOverride: true, isDynamic: true }, workspaceId);
|
|
3225
|
+
}
|
|
3226
|
+
function evidenceWithRuntimeVariables(evidence, vars) {
|
|
3227
|
+
const substitutions = runtimeSubstitutions(evidence, vars ?? {});
|
|
3228
|
+
const next = { ...evidence, runtimeSubstitutions: substitutions };
|
|
3229
|
+
for (const [key, value] of Object.entries(substitutions)) if (value.effective) next[key] = value.effective;
|
|
3230
|
+
const missing = Object.values(substitutions).flatMap((value) => value.missing);
|
|
3231
|
+
if (missing.length > 0) next.missingRuntimeVariables = [...new Set(missing)].sort();
|
|
3232
|
+
if (Object.keys(vars ?? {}).length > 0) next.runtimeVariablesApplied = true;
|
|
3233
|
+
return next;
|
|
3234
|
+
}
|
|
3235
|
+
function runtimeSubstitutions(evidence, vars) {
|
|
3236
|
+
const substitutions = {};
|
|
3237
|
+
for (const key of ["servicePath", "operationPath", "serviceAliasExpr", "serviceAlias", "destination"]) {
|
|
3238
|
+
const substitution = substituteVariables(stringValue3(evidence[key]), vars);
|
|
3239
|
+
if (substitution.placeholders.length > 0) substitutions[key] = substitution;
|
|
3240
|
+
}
|
|
3241
|
+
return substitutions;
|
|
3242
|
+
}
|
|
3243
|
+
function isRemoteRuntimeCandidate(row, evidence, vars) {
|
|
3244
|
+
if (!vars || Object.keys(vars).length === 0) return false;
|
|
3245
|
+
if (!["dynamic", "ambiguous", "unresolved"].includes(String(row.status ?? ""))) return false;
|
|
3246
|
+
if (!["DYNAMIC_EDGE_CANDIDATE", "UNRESOLVED_EDGE", "REMOTE_CALL_RESOLVES_TO_OPERATION"].includes(row.edge_type)) return false;
|
|
3247
|
+
return ["servicePath", "operationPath", "serviceAliasExpr", "serviceAlias", "destination"].some((key) => hasRuntimeVariable(evidence[key], vars));
|
|
3248
|
+
}
|
|
3249
|
+
function hasRuntimeVariable(value, vars) {
|
|
3250
|
+
return typeof value === "string" && extractPlaceholders(value).some((key) => Object.hasOwn(vars, key));
|
|
3251
|
+
}
|
|
3252
|
+
function runtimeUnresolvedReason(resolution) {
|
|
3253
|
+
if (resolution.status === "dynamic") return `Dynamic target is missing runtime variables: ${resolution.reasons.join(", ")}`;
|
|
3254
|
+
if (resolution.status === "ambiguous") return "Ambiguous runtime operation candidates";
|
|
3255
|
+
return "No runtime operation candidate matched substituted service and operation path";
|
|
3256
|
+
}
|
|
3257
|
+
function parseObject(value) {
|
|
3258
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3259
|
+
}
|
|
3260
|
+
function stringValue3(value) {
|
|
3261
|
+
return typeof value === "string" ? value : void 0;
|
|
3262
|
+
}
|
|
3263
|
+
|
|
2771
3264
|
// src/trace/trace-engine.ts
|
|
2772
3265
|
function normalizeOperation(value) {
|
|
2773
3266
|
if (!value) return void 0;
|
|
@@ -2776,7 +3269,7 @@ function normalizeOperation(value) {
|
|
|
2776
3269
|
function positiveDepth(value) {
|
|
2777
3270
|
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 25;
|
|
2778
3271
|
}
|
|
2779
|
-
function operationStartScope(db, repoId, start) {
|
|
3272
|
+
function operationStartScope(db, repoId, start, hintOptions) {
|
|
2780
3273
|
const requested = normalizeOperation(start.operationPath ?? start.operation);
|
|
2781
3274
|
if (!requested) return void 0;
|
|
2782
3275
|
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
|
|
@@ -2792,7 +3285,17 @@ function operationStartScope(db, repoId, start) {
|
|
|
2792
3285
|
const operationId = String(rows2[0]?.operationId);
|
|
2793
3286
|
const impl = implementationScope(db, operationId);
|
|
2794
3287
|
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: [] };
|
|
2795
|
-
|
|
3288
|
+
const hinted = implementationMethodIdFromHint(impl.edge, hintOptions);
|
|
3289
|
+
if (hinted.methodId) {
|
|
3290
|
+
const hintedScope = handlerScope(db, hinted.methodId);
|
|
3291
|
+
if (hintedScope?.files.size) return { files: hintedScope.files, symbols: hintedScope.symbolId ? /* @__PURE__ */ new Set([hintedScope.symbolId]) : void 0, operationId, diagnostics: [] };
|
|
3292
|
+
}
|
|
3293
|
+
if (impl.edge) {
|
|
3294
|
+
const evidence = parseEvidence(impl.edge.evidence_json);
|
|
3295
|
+
const hintDiagnostic = implementationHintDiagnostic(hinted);
|
|
3296
|
+
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, candidates: evidence.candidates }];
|
|
3297
|
+
return { operationId, diagnostics: hintDiagnostic ? [hintDiagnostic, ...diagnostics] : diagnostics };
|
|
3298
|
+
}
|
|
2796
3299
|
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" }] };
|
|
2797
3300
|
}
|
|
2798
3301
|
function sourceFilesForStart(db, repoId, start) {
|
|
@@ -2835,12 +3338,12 @@ function sourceFilesForStart(db, repoId, start) {
|
|
|
2835
3338
|
}
|
|
2836
3339
|
return void 0;
|
|
2837
3340
|
}
|
|
2838
|
-
function startScope(db, start) {
|
|
3341
|
+
function startScope(db, start, hintOptions) {
|
|
2839
3342
|
const repo = start.repo ? db.prepare(
|
|
2840
3343
|
"SELECT id,name FROM repositories WHERE name=? OR package_name=?"
|
|
2841
3344
|
).get(start.repo, start.repo) : void 0;
|
|
2842
3345
|
if (start.repo && !repo) return { repo, selectorMatched: false };
|
|
2843
|
-
const operationScope = operationStartScope(db, repo?.id, start);
|
|
3346
|
+
const operationScope = operationStartScope(db, repo?.id, start, hintOptions);
|
|
2844
3347
|
const terminalOperationScope = operationScope && !operationScope.files && (operationScope.diagnostics ?? []).some((d) => d.resolutionStage === "operation" || d.resolutionStage === "implementation");
|
|
2845
3348
|
const sourceScope = operationScope?.files || terminalOperationScope ? operationScope : sourceFilesForStart(db, repo?.id, start);
|
|
2846
3349
|
const sourceFiles = sourceScope?.files;
|
|
@@ -2887,8 +3390,15 @@ function implementationScope(db, operationId) {
|
|
|
2887
3390
|
const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name WHERE hm.id=?").get(edge.to_id);
|
|
2888
3391
|
return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
|
|
2889
3392
|
}
|
|
2890
|
-
function
|
|
2891
|
-
if (!edge || edge.status !== "ambiguous"
|
|
3393
|
+
function implementationMethodIdFromHint(edge, options) {
|
|
3394
|
+
if (!edge || edge.status !== "ambiguous") return { blocksAutomatic: false, evidence: { status: "not_applicable" } };
|
|
3395
|
+
return selectImplementation(parseEvidence(edge.evidence_json), options.implementationHints, options.implementationRepo);
|
|
3396
|
+
}
|
|
3397
|
+
function contextImplementationMethodId(edge, callerRepoId, remoteEvidence, hintOptions) {
|
|
3398
|
+
const hinted = implementationMethodIdFromHint(edge, hintOptions);
|
|
3399
|
+
if (hinted.methodId) return hinted;
|
|
3400
|
+
if (hinted.blocksAutomatic) return hinted;
|
|
3401
|
+
if (!edge || edge.status !== "ambiguous" || callerRepoId === void 0) return hinted;
|
|
2892
3402
|
const evidence = JSON.parse(String(edge.evidence_json || "{}"));
|
|
2893
3403
|
const scores = (evidence.candidates ?? []).filter((item) => item.accepted).map((item) => {
|
|
2894
3404
|
const reasons = [];
|
|
@@ -2907,10 +3417,10 @@ function contextImplementationMethodId(edge, callerRepoId, remoteEvidence = {})
|
|
|
2907
3417
|
}
|
|
2908
3418
|
return { methodId: item.methodId, score, reasons, handlerPackage: item.handlerPackage, applicationPackage: item.applicationPackage };
|
|
2909
3419
|
}).sort((a, b) => b.score - a.score);
|
|
2910
|
-
if (scores.length === 0) return { evidence: { status: "not_applicable", candidateScores: [] } };
|
|
3420
|
+
if (scores.length === 0) return { blocksAutomatic: false, evidence: { status: "not_applicable", candidateScores: [] } };
|
|
2911
3421
|
const [first, second] = scores;
|
|
2912
|
-
if (first && first.methodId !== void 0 && first.score > 0 && (!second || first.score > second.score)) return { methodId: String(first.methodId), evidence: { status: "selected", selectedMethodId: first.methodId, candidateScores: scores } };
|
|
2913
|
-
return { evidence: { status: "tied", tieReason: scores.length > 1 ? "duplicate_helper_implementation_candidates" : "no_unique_materially_stronger_candidate", candidateScores: scores } };
|
|
3422
|
+
if (first && first.methodId !== void 0 && first.score > 0 && (!second || first.score > second.score)) return { methodId: String(first.methodId), blocksAutomatic: false, evidence: { status: "selected", selectedMethodId: first.methodId, candidateScores: scores } };
|
|
3423
|
+
return { blocksAutomatic: false, evidence: hinted.evidence.reason === "no_scoped_hint_matched_edge" ? hinted.evidence : { status: "tied", tieReason: scores.length > 1 ? "duplicate_helper_implementation_candidates" : "no_unique_materially_stronger_candidate", candidateScores: scores } };
|
|
2914
3424
|
}
|
|
2915
3425
|
function handlerScope(db, methodId) {
|
|
2916
3426
|
const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name WHERE hm.id=?").get(methodId);
|
|
@@ -2940,31 +3450,6 @@ function graphForCalls(db, callIds) {
|
|
|
2940
3450
|
}
|
|
2941
3451
|
return map;
|
|
2942
3452
|
}
|
|
2943
|
-
function hasRuntimeVariable(value, vars) {
|
|
2944
|
-
return typeof value === "string" && extractPlaceholders(value).some((key) => Object.hasOwn(vars, key));
|
|
2945
|
-
}
|
|
2946
|
-
function isRemoteRuntimeCandidate(row, evidence, vars) {
|
|
2947
|
-
if (!vars || Object.keys(vars).length === 0) return false;
|
|
2948
|
-
if (!["dynamic", "ambiguous", "unresolved"].includes(String(row.status ?? ""))) return false;
|
|
2949
|
-
if (!["DYNAMIC_EDGE_CANDIDATE", "UNRESOLVED_EDGE", "REMOTE_CALL_RESOLVES_TO_OPERATION"].includes(row.edge_type)) return false;
|
|
2950
|
-
if (row.status === "resolved") return false;
|
|
2951
|
-
return ["servicePath", "operationPath", "serviceAliasExpr", "serviceAlias", "destination"].some((key) => hasRuntimeVariable(evidence[key], vars));
|
|
2952
|
-
}
|
|
2953
|
-
function evidenceWithRuntimeVariables(evidence, vars) {
|
|
2954
|
-
if (!vars || Object.keys(vars).length === 0) return evidence;
|
|
2955
|
-
const substitutions = {};
|
|
2956
|
-
for (const key of ["servicePath", "operationPath", "serviceAliasExpr", "serviceAlias", "destination"]) {
|
|
2957
|
-
const substitution = substituteVariables(typeof evidence[key] === "string" ? String(evidence[key]) : void 0, vars);
|
|
2958
|
-
if (substitution.placeholders.length > 0) substitutions[key] = substitution;
|
|
2959
|
-
}
|
|
2960
|
-
const next = { ...evidence, runtimeVariablesApplied: true, runtimeSubstitutions: substitutions };
|
|
2961
|
-
for (const [key, value] of Object.entries(substitutions)) {
|
|
2962
|
-
if (value.effective) next[key] = value.effective;
|
|
2963
|
-
}
|
|
2964
|
-
const missing = Object.values(substitutions).flatMap((value) => value.missing);
|
|
2965
|
-
if (missing.length > 0) next.missingRuntimeVariables = [...new Set(missing)];
|
|
2966
|
-
return next;
|
|
2967
|
-
}
|
|
2968
3453
|
function symbolNode(db, symbolId) {
|
|
2969
3454
|
const row = db.prepare(`SELECT s.id symbolId,s.name symbolName,s.qualified_name qualifiedName,s.source_file sourceFile,s.start_line startLine,s.end_line endLine,r.name repoName,r.id repoId FROM symbols s JOIN repositories r ON r.id=s.repo_id WHERE s.id=?`).get(symbolId);
|
|
2970
3455
|
if (!row) return void 0;
|
|
@@ -2979,24 +3464,6 @@ function operationNode(db, operationId) {
|
|
|
2979
3464
|
function workspaceIdForCall(db, callId) {
|
|
2980
3465
|
return db.prepare("SELECT r.workspace_id workspaceId FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE c.id=?").get(callId)?.workspaceId;
|
|
2981
3466
|
}
|
|
2982
|
-
function runtimeResolution(db, row, evidence, vars) {
|
|
2983
|
-
if (!isRemoteRuntimeCandidate(row, evidence, vars))
|
|
2984
|
-
return { row, evidence, unresolvedReason: row.unresolved_reason };
|
|
2985
|
-
const nextEvidence = evidenceWithRuntimeVariables(evidence, vars);
|
|
2986
|
-
const servicePath = typeof nextEvidence.servicePath === "string" ? nextEvidence.servicePath : void 0;
|
|
2987
|
-
const operationPath = typeof nextEvidence.normalizedOperationPath === "string" ? nextEvidence.normalizedOperationPath : typeof nextEvidence.operationPath === "string" ? nextEvidence.operationPath : void 0;
|
|
2988
|
-
const alias = typeof nextEvidence.serviceAliasExpr === "string" ? nextEvidence.serviceAliasExpr : typeof nextEvidence.serviceAlias === "string" ? nextEvidence.serviceAlias : void 0;
|
|
2989
|
-
const destination = typeof nextEvidence.destination === "string" ? nextEvidence.destination : void 0;
|
|
2990
|
-
const resolution = resolveOperation(db, { servicePath, operationPath, alias, destination, hasExplicitOverride: true, isDynamic: true }, workspaceIdForCall(db, row.from_id));
|
|
2991
|
-
nextEvidence.runtimeResolutionStatus = resolution.status;
|
|
2992
|
-
nextEvidence.runtimeResolutionReasons = resolution.reasons;
|
|
2993
|
-
if (resolution.target) {
|
|
2994
|
-
nextEvidence.runtimeResolvedCandidate = resolution.target;
|
|
2995
|
-
return { row: { ...row, to_kind: "operation", to_id: String(resolution.target.operationId), unresolved_reason: void 0, confidence: Math.max(0, Math.min(1, resolution.target.score)) }, evidence: nextEvidence, target: resolution.target };
|
|
2996
|
-
}
|
|
2997
|
-
const unresolvedReason = resolution.status === "dynamic" ? `Dynamic target is missing runtime variables: ${resolution.reasons.join(", ")}` : resolution.status === "ambiguous" ? "Ambiguous runtime operation candidates" : "No runtime operation candidate matched substituted service and operation path";
|
|
2998
|
-
return { row, evidence: nextEvidence, unresolvedReason };
|
|
2999
|
-
}
|
|
3000
3467
|
function parseEvidence(value) {
|
|
3001
3468
|
try {
|
|
3002
3469
|
const parsed = JSON.parse(String(value || "{}"));
|
|
@@ -3051,18 +3518,22 @@ function contextForSymbolCall(db, symbolCall, callerBindings) {
|
|
|
3051
3518
|
const next = /* @__PURE__ */ new Map();
|
|
3052
3519
|
if (callerBindings.size === 0) return next;
|
|
3053
3520
|
const callEvidence2 = parseEvidence(symbolCall.evidence_json);
|
|
3054
|
-
const callee = db.prepare("SELECT evidence_json evidenceJson FROM symbols WHERE id=?").get(symbolCall.callee_symbol_id);
|
|
3521
|
+
const callee = db.prepare("SELECT evidence_json evidenceJson,source_file sourceFile,start_line startLine FROM symbols WHERE id=?").get(symbolCall.callee_symbol_id);
|
|
3055
3522
|
const calleeEvidence = parseEvidence(callee?.evidenceJson);
|
|
3056
3523
|
const params = Array.isArray(calleeEvidence.parameters) ? calleeEvidence.parameters.filter((item) => typeof item === "string") : [];
|
|
3057
3524
|
const parameterBindings = Array.isArray(calleeEvidence.parameterBindings) ? calleeEvidence.parameterBindings.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
|
|
3058
3525
|
const parameterPropertyAliases = Array.isArray(calleeEvidence.parameterPropertyAliases) ? calleeEvidence.parameterPropertyAliases.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
|
|
3059
3526
|
const args = Array.isArray(callEvidence2.callArguments) ? callEvidence2.callArguments : [];
|
|
3527
|
+
const provenance = {
|
|
3528
|
+
callerSite: { sourceFile: String(symbolCall.source_file ?? ""), sourceLine: Number(symbolCall.source_line ?? 0) },
|
|
3529
|
+
calleeSite: { sourceFile: callee?.sourceFile, sourceLine: callee?.startLine }
|
|
3530
|
+
};
|
|
3060
3531
|
args.forEach((arg, index) => {
|
|
3061
3532
|
const paramBinding = parameterBindings.find((binding) => binding.index === index);
|
|
3062
3533
|
const param = paramBinding?.kind === "identifier" && typeof paramBinding.name === "string" ? paramBinding.name : params[index];
|
|
3063
3534
|
if (arg.kind === "identifier" && typeof arg.name === "string") {
|
|
3064
3535
|
const binding = callerBindings.get(arg.name);
|
|
3065
|
-
if (binding && param) next.set(param, { ...binding, source: "local_symbol_argument", callerArgument: arg.name, calleeParameter: param, calleeReceiver: param });
|
|
3536
|
+
if (binding && param) next.set(param, { ...binding, ...provenance, source: "local_symbol_argument", callerArgument: arg.name, calleeParameter: param, calleeReceiver: param });
|
|
3066
3537
|
}
|
|
3067
3538
|
if (arg.kind === "object_literal" && Array.isArray(arg.properties)) {
|
|
3068
3539
|
for (const prop of arg.properties) {
|
|
@@ -3070,15 +3541,23 @@ function contextForSymbolCall(db, symbolCall, callerBindings) {
|
|
|
3070
3541
|
const binding = callerBindings.get(prop.argument);
|
|
3071
3542
|
if (!binding) continue;
|
|
3072
3543
|
const destructured = paramBinding?.kind === "object_pattern" && Array.isArray(paramBinding.properties) ? paramBinding.properties.find((item) => item.property === prop.property && typeof item.local === "string") : void 0;
|
|
3073
|
-
if (destructured && typeof destructured.local === "string") next.set(destructured.local, { ...binding, source: "local_symbol_destructured_object_argument", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: String(index), calleeReceiver: destructured.local });
|
|
3544
|
+
if (destructured && typeof destructured.local === "string") next.set(destructured.local, { ...binding, ...provenance, source: "local_symbol_destructured_object_argument", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: String(index), calleeReceiver: destructured.local });
|
|
3074
3545
|
else if (param) {
|
|
3075
|
-
next.set(`${param}.${prop.property}`, { ...binding, source: "local_symbol_object_argument", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeReceiver: `${param}.${prop.property}` });
|
|
3546
|
+
next.set(`${param}.${prop.property}`, { ...binding, ...provenance, source: "local_symbol_object_argument", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeReceiver: `${param}.${prop.property}` });
|
|
3076
3547
|
for (const alias of parameterPropertyAliases) {
|
|
3077
|
-
if (alias.parameter === param && alias.property === prop.property && typeof alias.local === "string") next.set(alias.local, { ...binding, source: "local_symbol_object_parameter_destructure", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeObjectProperty: `${param}.${prop.property}`, calleeReceiver: alias.local, calleeLocalDestructuredIdentifier: alias.local, parameterPropertyAliasKind: alias.kind, parameterPropertyAliasLine: alias.line });
|
|
3548
|
+
if (alias.parameter === param && alias.property === prop.property && typeof alias.local === "string") next.set(alias.local, { ...binding, ...provenance, source: "local_symbol_object_parameter_destructure", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeObjectProperty: `${param}.${prop.property}`, calleeReceiver: alias.local, calleeLocalDestructuredIdentifier: alias.local, parameterPropertyAliasKind: alias.kind, parameterPropertyAliasLine: alias.line });
|
|
3078
3549
|
}
|
|
3079
3550
|
}
|
|
3080
3551
|
}
|
|
3081
3552
|
}
|
|
3553
|
+
if (arg.kind === "array_literal" && Array.isArray(arg.elements) && paramBinding?.kind === "array_pattern" && Array.isArray(paramBinding.elements)) {
|
|
3554
|
+
for (const element of arg.elements) {
|
|
3555
|
+
const target = paramBinding.elements.find((item) => item.index === element.index);
|
|
3556
|
+
if (element.kind !== "identifier" || typeof element.name !== "string" || typeof target?.local !== "string") continue;
|
|
3557
|
+
const binding = callerBindings.get(element.name);
|
|
3558
|
+
if (binding) next.set(target.local, { ...binding, ...provenance, source: "local_symbol_destructured_array_argument", callerArgument: element.name, calleeParameter: String(index), calleeReceiver: target.local });
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3082
3561
|
});
|
|
3083
3562
|
return next;
|
|
3084
3563
|
}
|
|
@@ -3089,34 +3568,16 @@ function contextualRuntimeResolution(db, call, binding, workspaceId, persistedRo
|
|
|
3089
3568
|
const servicePath = binding.effectiveServicePath ?? binding.servicePathExpr ?? binding.requireServicePath;
|
|
3090
3569
|
const destination = binding.effectiveDestination ?? binding.destinationExpr ?? binding.requireDestination;
|
|
3091
3570
|
const resolution = resolveOperation(db, { servicePath, operationPath: op, alias: binding.aliasExpr ?? binding.alias, destination, hasExplicitOverride: true, isDynamic: false }, workspaceId);
|
|
3092
|
-
const evidence = { contextualServiceBindingAttempted: true, contextualBinding: { source: binding.source, callerArgument: binding.callerArgument, callerProperty: binding.callerProperty, calleeParameter: binding.calleeParameter, calleeReceiver: binding.calleeReceiver, bindingSourceFile: binding.sourceFile, bindingSourceLine: binding.sourceLine, alias: binding.alias, aliasExpr: binding.aliasExpr, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination }, operationPath: op, rawOperationPath: normalized?.rawOperationPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : void 0, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : void 0, servicePath, serviceAlias: binding.alias, serviceAliasExpr: binding.aliasExpr, destination, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination, contextualResolutionStatus: resolution.status, contextualCandidateCount: resolution.candidates.length, candidates: resolution.candidates, contextualResolutionReasons: resolution.reasons, resolutionReasons: resolution.reasons };
|
|
3571
|
+
const evidence = { contextualServiceBindingAttempted: true, contextualBinding: { source: binding.source, callerArgument: binding.callerArgument, callerProperty: binding.callerProperty, calleeParameter: binding.calleeParameter, calleeReceiver: binding.calleeReceiver, callerSite: binding.callerSite, calleeSite: binding.calleeSite, bindingSourceFile: binding.sourceFile, bindingSourceLine: binding.sourceLine, alias: binding.alias, aliasExpr: binding.aliasExpr, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination }, operationPath: op, rawOperationPath: normalized?.rawOperationPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : void 0, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : void 0, servicePath, serviceAlias: binding.alias, serviceAliasExpr: binding.aliasExpr, destination, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination, contextualResolutionStatus: resolution.status, contextualCandidateCount: resolution.candidates.length, candidates: resolution.candidates, contextualResolutionReasons: resolution.reasons, resolutionReasons: resolution.reasons };
|
|
3093
3572
|
if (!resolution.target) return { evidence, unresolvedReason: resolution.status === "ambiguous" ? "Ambiguous contextual operation candidates" : resolution.status === "dynamic" ? `Dynamic contextual target is missing runtime variables: ${resolution.reasons.join(", ")}` : "No contextual operation candidate matched" };
|
|
3094
3573
|
const resolvedEvidence = { ...evidence, contextualServiceBindingSelected: true, targetRepo: resolution.target.repoName, targetServicePath: resolution.target.servicePath, targetOperationPath: resolution.target.operationPath, targetOperation: resolution.target.operationName };
|
|
3095
3574
|
const persistedResolved = persistedRows.find((item) => item.status === "resolved");
|
|
3096
3575
|
if (persistedResolved) return { row: void 0, evidence: { ...resolvedEvidence, contextualPreservedPersistedResolvedEdge: true }, unresolvedReason: void 0 };
|
|
3097
3576
|
return { row: { id: -Number(call.id), edge_type: "REMOTE_CALL_RESOLVES_TO_OPERATION", from_id: String(call.id), to_kind: "operation", to_id: String(resolution.target.operationId), confidence: resolution.target.score, evidence_json: JSON.stringify(resolvedEvidence), status: "resolved" }, evidence: resolvedEvidence, unresolvedReason: void 0 };
|
|
3098
3577
|
}
|
|
3099
|
-
function edgeTarget(row, evidence) {
|
|
3100
|
-
const runtimeCandidate = evidence.runtimeResolvedCandidate;
|
|
3101
|
-
if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath)
|
|
3102
|
-
return `${runtimeCandidate.servicePath}${runtimeCandidate.operationPath}`;
|
|
3103
|
-
const targetServicePath = typeof evidence.targetServicePath === "string" ? evidence.targetServicePath : void 0;
|
|
3104
|
-
const targetOperationPath = typeof evidence.targetOperationPath === "string" ? evidence.targetOperationPath : void 0;
|
|
3105
|
-
if (targetServicePath && targetOperationPath) return `${targetServicePath}${targetOperationPath}`;
|
|
3106
|
-
const servicePath = typeof evidence.servicePath === "string" ? evidence.servicePath : void 0;
|
|
3107
|
-
const operationPath = typeof evidence.operationPath === "string" ? evidence.operationPath : void 0;
|
|
3108
|
-
const targetOperation = typeof evidence.targetOperation === "string" ? evidence.targetOperation : void 0;
|
|
3109
|
-
const targetRepo = typeof evidence.targetRepo === "string" ? evidence.targetRepo : "";
|
|
3110
|
-
if (row.edge_type === "HANDLER_RUNS_DB_QUERY") return `Entity: ${row.to_id || "unknown"}`;
|
|
3111
|
-
if (row.edge_type === "HANDLER_RUNS_REMOTE_QUERY") return typeof evidence.remoteQueryTarget === "string" ? evidence.remoteQueryTarget : `Remote query: ${row.to_id || "unknown"}`;
|
|
3112
|
-
if (row.edge_type === "HANDLER_CALLS_EXTERNAL_HTTP") {
|
|
3113
|
-
const target = evidence.externalTarget;
|
|
3114
|
-
return typeof target?.label === "string" ? target.label : `External endpoint: ${row.to_id || "unknown"}`;
|
|
3115
|
-
}
|
|
3116
|
-
return servicePath && operationPath ? `${servicePath}${operationPath}` : targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
|
|
3117
|
-
}
|
|
3118
3578
|
function trace(db, start, options) {
|
|
3119
|
-
const
|
|
3579
|
+
const hintOptions = { implementationRepo: options.implementationRepo, implementationHints: options.implementationHints };
|
|
3580
|
+
const scope = startScope(db, start, hintOptions);
|
|
3120
3581
|
const diagnostics = db.prepare(
|
|
3121
3582
|
"SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics WHERE (? IS NULL OR repo_id=?)"
|
|
3122
3583
|
).all(scope.repo?.id, scope.repo?.id);
|
|
@@ -3139,12 +3600,14 @@ function trace(db, start, options) {
|
|
|
3139
3600
|
const op = operationNode(db, scope.startOperationId);
|
|
3140
3601
|
const impl = implementationScope(db, scope.startOperationId);
|
|
3141
3602
|
if (op) nodes.set(String(op.id), op);
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
const
|
|
3603
|
+
const startSelection = implementationMethodIdFromHint(impl.edge, hintOptions);
|
|
3604
|
+
if (impl.edge && (impl.edge.status === "resolved" || startSelection.methodId)) {
|
|
3605
|
+
const selectedMethodId = impl.edge.status === "resolved" ? impl.edge.to_id : startSelection.methodId;
|
|
3606
|
+
const implEvidence = { ...parseEvidence(impl.edge.evidence_json), startResolution: { strategy: "indexed_operation_graph", matchedOperationId: scope.startOperationId, implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, selectedHandlerMethodId: selectedMethodId }, implementationSelection: startSelection.methodId ? startSelection.evidence : void 0 };
|
|
3607
|
+
const handlerNode = selectedMethodId ? handlerMethodNode(db, selectedMethodId) : void 0;
|
|
3145
3608
|
if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
|
|
3146
3609
|
seenEdges.add(Number(impl.edge.id));
|
|
3147
|
-
edges.push({ step: 1, type: "operation_implemented_by_handler", from: op?.label ? String(op.label) : `operation:${scope.startOperationId}`, to: handlerNode?.label ? String(handlerNode.label) : `${impl.edge.to_kind}:${impl.edge.to_id}`, evidence: implEvidence, confidence: Number(impl.edge.confidence ?? 0), unresolvedReason: impl.edge.status === "resolved" ? void 0 : String(impl.edge.unresolved_reason ?? impl.edge.status) });
|
|
3610
|
+
edges.push({ step: 1, type: "operation_implemented_by_handler", from: op?.label ? String(op.label) : `operation:${scope.startOperationId}`, to: handlerNode?.label ? String(handlerNode.label) : `${impl.edge.to_kind}:${impl.edge.to_id}`, evidence: implEvidence, confidence: Number(impl.edge.confidence ?? 0), unresolvedReason: impl.edge.status === "resolved" || startSelection.methodId ? void 0 : String(impl.edge.unresolved_reason ?? impl.edge.status) });
|
|
3148
3611
|
}
|
|
3149
3612
|
}
|
|
3150
3613
|
const seenScopes = /* @__PURE__ */ new Set();
|
|
@@ -3201,8 +3664,8 @@ function trace(db, start, options) {
|
|
|
3201
3664
|
const persistedEvidence = JSON.parse(
|
|
3202
3665
|
String(row.evidence_json || "{}")
|
|
3203
3666
|
);
|
|
3204
|
-
const rawEvidence =
|
|
3205
|
-
const effective = runtimeResolution(db, row, rawEvidence, options.vars);
|
|
3667
|
+
const rawEvidence = baseTraceEvidence(row, call, persistedEvidence, contextual.evidence);
|
|
3668
|
+
const effective = runtimeResolution(db, row, rawEvidence, options.vars, workspaceIdForCall(db, String(call.id)));
|
|
3206
3669
|
const evidence = effective.evidence;
|
|
3207
3670
|
const effectiveRow = effective.row;
|
|
3208
3671
|
const targetNode = `${effectiveRow.to_kind}:${effectiveRow.to_id}`;
|
|
@@ -3224,10 +3687,12 @@ function trace(db, start, options) {
|
|
|
3224
3687
|
});
|
|
3225
3688
|
if (effectiveRow.to_kind === "operation") {
|
|
3226
3689
|
const implementation = implementationScope(db, effectiveRow.to_id);
|
|
3227
|
-
const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence);
|
|
3690
|
+
const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence, hintOptions);
|
|
3228
3691
|
const contextMethodId = contextSelection.methodId;
|
|
3229
3692
|
const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : void 0;
|
|
3230
3693
|
if (implementation.edge) {
|
|
3694
|
+
const hintDiagnostic = implementationHintDiagnostic(contextSelection);
|
|
3695
|
+
if (hintDiagnostic) diagnostics.unshift(hintDiagnostic);
|
|
3231
3696
|
const implEvidence = JSON.parse(String(implementation.edge.evidence_json || "{}"));
|
|
3232
3697
|
const handlerNode = implementation.edge.status === "resolved" ? handlerMethodNode(db, implementation.edge.to_id) : contextNode;
|
|
3233
3698
|
const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
|
|
@@ -3237,7 +3702,7 @@ function trace(db, start, options) {
|
|
|
3237
3702
|
type: "operation_implemented_by_handler",
|
|
3238
3703
|
from: to,
|
|
3239
3704
|
to: implTo,
|
|
3240
|
-
evidence: contextMethodId ? { ...implEvidence, contextualImplementationSelected:
|
|
3705
|
+
evidence: contextMethodId ? { ...implEvidence, contextualImplementationSelected: contextSelection.evidence.strategy !== "implementation_repo_hint", contextualImplementation: contextSelection.evidence, implementationSelection: contextSelection.evidence } : { ...implEvidence, contextualImplementation: contextSelection.evidence, implementationSelection: contextSelection.evidence },
|
|
3241
3706
|
confidence: Number(implementation.edge.confidence ?? 0),
|
|
3242
3707
|
unresolvedReason: implementation.edge.status === "resolved" || contextMethodId ? void 0 : String(implementation.edge.unresolved_reason ?? implementation.edge.status)
|
|
3243
3708
|
});
|
|
@@ -3273,6 +3738,8 @@ function trace(db, start, options) {
|
|
|
3273
3738
|
}
|
|
3274
3739
|
}
|
|
3275
3740
|
}
|
|
3741
|
+
const runtimeDiagnostic = runtimeVariableDiagnostic(edges);
|
|
3742
|
+
if (runtimeDiagnostic) diagnostics.unshift(runtimeDiagnostic);
|
|
3276
3743
|
return { start, nodes: [...nodes.values()], edges, diagnostics };
|
|
3277
3744
|
}
|
|
3278
3745
|
|
|
@@ -3288,13 +3755,14 @@ export {
|
|
|
3288
3755
|
redactValue,
|
|
3289
3756
|
normalizeODataOperationInvocationPath,
|
|
3290
3757
|
classifyODataPathIntent,
|
|
3758
|
+
parseServiceBindings,
|
|
3291
3759
|
containsSupportedOutboundCall,
|
|
3292
3760
|
parseOutboundCalls,
|
|
3293
|
-
parseServiceBindings,
|
|
3294
3761
|
applyVariables,
|
|
3295
3762
|
extractPlaceholders,
|
|
3296
3763
|
substituteVariables,
|
|
3297
3764
|
linkWorkspace,
|
|
3765
|
+
parseImplementationHint,
|
|
3298
3766
|
trace
|
|
3299
3767
|
};
|
|
3300
|
-
//# sourceMappingURL=chunk-
|
|
3768
|
+
//# sourceMappingURL=chunk-BXSCE5CR.js.map
|