@saptools/service-flow 0.1.44 → 0.1.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +9 -1
- package/dist/{chunk-UKNPHTUS.js → chunk-EGY2A4AT.js} +1853 -1462
- package/dist/chunk-EGY2A4AT.js.map +1 -0
- package/dist/cli.js +101 -31
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/doctor.ts +67 -13
- package/src/cli.ts +10 -2
- package/src/index.ts +2 -0
- package/src/linker/cross-repo-linker.ts +27 -3
- package/src/linker/odata-path-normalizer.ts +4 -1
- package/src/output/table-output.ts +16 -1
- package/src/parsers/imported-wrapper-parser.ts +262 -0
- package/src/parsers/outbound-call-parser.ts +5 -1
- package/src/parsers/symbol-parser.ts +25 -10
- package/src/trace/evidence.ts +6 -1
- package/src/trace/implementation-hints.ts +223 -0
- package/src/trace/trace-engine.ts +52 -45
- package/src/types.ts +8 -0
- package/dist/chunk-UKNPHTUS.js.map +0 -1
|
@@ -577,1518 +577,1716 @@ function summarizeExpression(text) {
|
|
|
577
577
|
return redactText(text).slice(0, 240);
|
|
578
578
|
}
|
|
579
579
|
|
|
580
|
-
// src/parsers/
|
|
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
|
|
581
585
|
import fs6 from "fs/promises";
|
|
582
586
|
import path7 from "path";
|
|
583
587
|
import ts4 from "typescript";
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
import { createHash } from "crypto";
|
|
587
|
-
var sensitiveKeys = /* @__PURE__ */ new Set(["token", "access_token", "id_token", "api_key", "apikey", "key", "password", "passwd", "pwd", "secret", "client_secret", "authorization", "cookie", "signature"]);
|
|
588
|
-
function hash(value) {
|
|
589
|
-
return createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
588
|
+
function lineOf3(sf, node) {
|
|
589
|
+
return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
590
590
|
}
|
|
591
|
-
function
|
|
592
|
-
|
|
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();
|
|
593
596
|
}
|
|
594
|
-
function
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
+
}
|
|
604
629
|
}
|
|
630
|
+
visitObject(objectArg);
|
|
631
|
+
return out;
|
|
605
632
|
}
|
|
606
|
-
function
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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;
|
|
616
662
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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);
|
|
621
668
|
}
|
|
622
|
-
|
|
623
|
-
return
|
|
669
|
+
visit(expr);
|
|
670
|
+
return found;
|
|
624
671
|
}
|
|
625
|
-
function
|
|
672
|
+
async function readSource(abs) {
|
|
626
673
|
try {
|
|
627
|
-
const
|
|
628
|
-
return
|
|
674
|
+
const text = await fs6.readFile(abs, "utf8");
|
|
675
|
+
return ts4.createSourceFile(abs, text, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
|
|
629
676
|
} catch {
|
|
630
|
-
return
|
|
677
|
+
return void 0;
|
|
631
678
|
}
|
|
632
679
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
if (query >= 0) return rejected("query_string_paths_are_not_operation_invocations");
|
|
644
|
-
if (!raw.startsWith("/")) return rejected("path_is_not_absolute");
|
|
645
|
-
if (raw.slice(1, open).includes("/")) return rejected("operation_segment_contains_navigation_separator");
|
|
646
|
-
const close = matchingClose(raw, open);
|
|
647
|
-
if (close === void 0) return rejected("top_level_invocation_parenthesis_is_unbalanced");
|
|
648
|
-
if (raw.slice(close + 1).trim().length > 0) return rejected("top_level_invocation_does_not_cover_remaining_path");
|
|
649
|
-
const operationSegment = raw.slice(0, open).trim();
|
|
650
|
-
if (operationSegment.length <= 1) return rejected("operation_segment_is_empty");
|
|
651
|
-
return {
|
|
652
|
-
rawOperationPath: raw,
|
|
653
|
-
normalizedOperationPath: operationSegment,
|
|
654
|
-
wasInvocation: true,
|
|
655
|
-
invocationArgumentPlaceholderKeys: [...new Set(extractTemplatePlaceholders(raw.slice(open + 1, close)))],
|
|
656
|
-
normalizationReason: "balanced_top_level_operation_invocation"
|
|
657
|
-
};
|
|
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;
|
|
658
690
|
}
|
|
659
|
-
function
|
|
660
|
-
const
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
const firstOpen = firstSegment.indexOf("(");
|
|
671
|
-
const firstClose = firstOpen >= 0 ? matchingClose(firstSegment, firstOpen) : void 0;
|
|
672
|
-
const keyPredicateText = firstOpen >= 0 && firstClose !== void 0 ? firstSegment.slice(firstOpen + 1, firstClose) : "";
|
|
673
|
-
const rawKeyPredicatePlaceholderKeys = [...new Set(extractTemplatePlaceholders(keyPredicateText))];
|
|
674
|
-
const navigationSuffix = hasNavigationSegments ? segments.slice(1).join("/") : void 0;
|
|
675
|
-
const lastSegment = segments.at(-1) ?? "";
|
|
676
|
-
const mediaOrPropertySuffix = hasNavigationSegments ? lastSegment : void 0;
|
|
677
|
-
const invocation = normalizeODataOperationInvocationPath(pathWithoutQuery);
|
|
678
|
-
const operationNameFromInvocation = invocation?.wasInvocation ? invocation.normalizedOperationPath.replace(/^\//, "").split(".").at(-1) : void 0;
|
|
679
|
-
const topLevelName = firstSegment.split("(")[0]?.replace(/^\//, "");
|
|
680
|
-
const topLevelOperationName = operationNameFromInvocation ?? (!hasNavigationSegments && !firstSegment.includes("(") ? topLevelName : void 0);
|
|
681
|
-
const topLevelOperationInvocation = Boolean(invocation?.wasInvocation && looksLikeLowerCamelInvocation(firstSegment));
|
|
682
|
-
const keyPredicatePlaceholderKeys = topLevelOperationInvocation ? [] : rawKeyPredicatePlaceholderKeys;
|
|
683
|
-
const topLevelOperationNameCandidate = Boolean(topLevelOperationName && !hasNavigationSegments && !firstSegment.includes("("));
|
|
684
|
-
const base = { rawPath, method: normalizedMethod, pathWithoutQuery, queryString, hasQueryString: queryIndex >= 0, entitySegment, placeholderKeys, keyPredicatePlaceholderKeys, invocationArgumentPlaceholderKeys: invocation?.invocationArgumentPlaceholderKeys ?? [], navigationSuffix, mediaOrPropertySuffix, hasEntityKeyPredicate: firstOpen >= 0 && firstClose !== void 0, hasNavigationSuffix: hasNavigationSegments, hasMediaOrPropertySuffix: Boolean(mediaOrPropertySuffix && isMediaOrPropertySuffix(mediaOrPropertySuffix)), topLevelOperationName, topLevelOperationNameCandidate, topLevelOperationInvocation };
|
|
685
|
-
if (!rawPath || !rawPath.startsWith("/")) return { ...base, kind: "unknown", reason: "path_missing_or_not_absolute" };
|
|
686
|
-
const upperEntityLike = /^[A-Z][A-Za-z0-9_]*$/.test(entitySegment ?? firstSegment);
|
|
687
|
-
const mediaLike = isMediaOrPropertySuffix(segments.at(-1) ?? "");
|
|
688
|
-
if (normalizedMethod !== "GET") {
|
|
689
|
-
if (invocation?.wasInvocation && looksLikeLowerCamelInvocation(firstSegment)) return { ...base, kind: "operation_invocation", reason: "non_get_balanced_top_level_operation_invocation" };
|
|
690
|
-
if (mediaLike) return { ...base, kind: "entity_media", reason: "non_get_entity_media_stream_path" };
|
|
691
|
-
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" };
|
|
692
|
-
if (upperEntityLike) return { ...base, kind: normalizedMethod === "DELETE" ? "entity_delete" : "entity_mutation", reason: "non_get_entity_path_shape" };
|
|
693
|
-
return { ...base, kind: "operation_invocation", reason: "non_get_lowercase_path_may_be_operation" };
|
|
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 });
|
|
694
702
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
703
|
+
return imports;
|
|
704
|
+
}
|
|
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 }] });
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
ts5.forEachChild(node, visit);
|
|
699
722
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
723
|
+
visit(fn);
|
|
724
|
+
return bindings;
|
|
725
|
+
}
|
|
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
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
ts5.forEachChild(node, visit);
|
|
704
742
|
}
|
|
705
|
-
|
|
706
|
-
|
|
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;
|
|
707
750
|
}
|
|
708
|
-
function
|
|
709
|
-
|
|
710
|
-
if (
|
|
711
|
-
|
|
712
|
-
const entity = (open >= 0 ? first.slice(0, open) : first).trim();
|
|
713
|
-
return entity || void 0;
|
|
751
|
+
function functionLikeInitializer(expr) {
|
|
752
|
+
if (!expr) return void 0;
|
|
753
|
+
if (ts5.isArrowFunction(expr) || ts5.isFunctionExpression(expr)) return expr;
|
|
754
|
+
return void 0;
|
|
714
755
|
}
|
|
715
|
-
function
|
|
716
|
-
|
|
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);
|
|
717
770
|
}
|
|
718
|
-
function
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
return /^[a-z][A-Za-z0-9_]*$/.test(name);
|
|
771
|
+
function directConnectFactFromFunctionLike(fn) {
|
|
772
|
+
if (ts5.isArrowFunction(fn) && fn.body && !ts5.isBlock(fn.body))
|
|
773
|
+
return findConnectInExpression(fn.body);
|
|
774
|
+
return directReturnConnectFact(fn);
|
|
723
775
|
}
|
|
724
|
-
function
|
|
725
|
-
const
|
|
726
|
-
for (
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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);
|
|
733
792
|
}
|
|
734
|
-
return
|
|
793
|
+
return exports;
|
|
735
794
|
}
|
|
736
|
-
function
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
}
|
|
750
|
-
if (quote === "single" && char === "'" || quote === "double" && char === '"' || quote === "template" && char === "`") quote = void 0;
|
|
751
|
-
continue;
|
|
752
|
-
}
|
|
753
|
-
if (char === "$" && text[index + 1] === "{") {
|
|
754
|
-
const close = matchingPlaceholderClose(text, index + 1);
|
|
755
|
-
if (close === void 0) return void 0;
|
|
756
|
-
index = close;
|
|
757
|
-
continue;
|
|
758
|
-
}
|
|
759
|
-
if (char === "'") {
|
|
760
|
-
quote = "single";
|
|
761
|
-
continue;
|
|
762
|
-
}
|
|
763
|
-
if (char === '"') {
|
|
764
|
-
quote = "double";
|
|
765
|
-
continue;
|
|
766
|
-
}
|
|
767
|
-
if (char === "`") {
|
|
768
|
-
quote = "template";
|
|
769
|
-
continue;
|
|
770
|
-
}
|
|
771
|
-
if (char === "(") depth += 1;
|
|
772
|
-
if (char === ")") {
|
|
773
|
-
depth -= 1;
|
|
774
|
-
if (depth === 0) return index;
|
|
775
|
-
if (depth < 0) return void 0;
|
|
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) });
|
|
776
808
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
+
});
|
|
792
834
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
if (
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
depth -= 1;
|
|
811
|
-
if (depth === 0) return index;
|
|
812
|
-
if (depth < 0) return void 0;
|
|
835
|
+
}
|
|
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
|
+
});
|
|
845
|
+
}
|
|
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 });
|
|
813
852
|
}
|
|
814
853
|
}
|
|
815
|
-
return
|
|
854
|
+
return out;
|
|
816
855
|
}
|
|
817
|
-
function
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
if (
|
|
836
|
-
index = close;
|
|
837
|
-
continue;
|
|
838
|
-
}
|
|
839
|
-
if (char === "'") {
|
|
840
|
-
quote = "single";
|
|
841
|
-
continue;
|
|
842
|
-
}
|
|
843
|
-
if (char === '"') {
|
|
844
|
-
quote = "double";
|
|
845
|
-
continue;
|
|
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);
|
|
846
875
|
}
|
|
847
|
-
if (
|
|
848
|
-
|
|
849
|
-
|
|
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
|
+
}
|
|
850
888
|
}
|
|
851
|
-
if (char === "?") return index;
|
|
852
889
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
if (ts4.isIdentifier(expr) || ts4.isStringLiteral(expr) || ts4.isNoSubstitutionTemplateLiteral(expr)) return expr.text;
|
|
863
|
-
if (ts4.isPropertyAccessExpression(expr) && expr.expression.kind === ts4.SyntaxKind.ThisKeyword) return expr.name.text;
|
|
864
|
-
if (ts4.isElementAccessExpression(expr) && expr.argumentExpression && (ts4.isStringLiteral(expr.argumentExpression) || ts4.isNoSubstitutionTemplateLiteral(expr.argumentExpression))) return expr.argumentExpression.text;
|
|
865
|
-
return void 0;
|
|
866
|
-
}
|
|
867
|
-
function expressionName(expr) {
|
|
868
|
-
if (ts4.isIdentifier(expr)) return expr.text;
|
|
869
|
-
if (ts4.isPropertyAccessExpression(expr)) return `${expressionName(expr.expression)}.${expr.name.text}`;
|
|
870
|
-
return expr.getText();
|
|
871
|
-
}
|
|
872
|
-
function variableInitializers(source) {
|
|
873
|
-
const initializers = /* @__PURE__ */ new Map();
|
|
874
|
-
for (const statement of source.statements) {
|
|
875
|
-
if (!ts4.isVariableStatement(statement) || (statement.declarationList.flags & ts4.NodeFlags.Const) === 0) continue;
|
|
876
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
877
|
-
if (ts4.isIdentifier(declaration.name) && declaration.initializer) initializers.set(declaration.name.text, declaration.initializer);
|
|
878
|
-
}
|
|
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 }));
|
|
879
899
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
function queryEntityFromAst(expr, initializers = /* @__PURE__ */ new Map()) {
|
|
883
|
-
if (ts4.isParenthesizedExpression(expr) || ts4.isAwaitExpression(expr)) return queryEntityFromAst(expr.expression, initializers);
|
|
884
|
-
if (ts4.isIdentifier(expr) && initializers.has(expr.text)) return queryEntityFromAst(initializers.get(expr.text), initializers);
|
|
885
|
-
if (ts4.isCallExpression(expr)) {
|
|
886
|
-
const name = expressionName(expr.expression);
|
|
887
|
-
if (name === "cds.run") return queryEntityFromAst(expr.arguments[0], initializers);
|
|
888
|
-
if (["SELECT.one.from", "SELECT.from", "SELECT.one", "INSERT.into", "UPSERT.into", "DELETE.from", "UPDATE.entity"].includes(name)) return entityFromExpression(expr.arguments[0]);
|
|
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);
|
|
900
|
+
async function importedHelper(localName) {
|
|
901
|
+
return (await importedHelpers(localName)).find((row) => !row.helper.returnedProperty);
|
|
892
902
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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;
|
|
903
|
+
function bindingForVariable(variableName) {
|
|
904
|
+
const sourceFile = normalizePath(filePath);
|
|
905
|
+
return [...out].reverse().find((row) => row.variableName === variableName && row.sourceFile === sourceFile);
|
|
960
906
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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"] };
|
|
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
|
+
});
|
|
1002
925
|
}
|
|
1003
|
-
|
|
1004
|
-
if (
|
|
1005
|
-
const
|
|
1006
|
-
if (!
|
|
1007
|
-
|
|
1008
|
-
seen.add(binding.declaration);
|
|
1009
|
-
const resolved2 = resolveExpression(binding.initializer, binding.declaration, policy, depth + 1, seen);
|
|
1010
|
-
return { ...resolved2, sourceKind: "const_alias", rawExpression: safeRaw(expr), constName: expr.text, evidence: [...binding.evidence, ...resolved2.evidence] };
|
|
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);
|
|
1011
931
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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
|
+
});
|
|
1047
971
|
}
|
|
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;
|
|
1078
972
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
const text = resolveExpression(propertyInitializer(object, "method"), use, "literal").value;
|
|
1083
|
-
return text ? stripQuotes(text).toUpperCase() : void 0;
|
|
1084
|
-
}
|
|
1085
|
-
var supportedHttpMethods = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
|
|
1086
|
-
function safeOperationName(value) {
|
|
1087
|
-
if (!value || !/^[A-Za-z_$][\w$]*(?:[./][A-Za-z_$][\w$]*)*$/.test(value)) return void 0;
|
|
1088
|
-
return operationPathFromStatic(value);
|
|
1089
|
-
}
|
|
1090
|
-
function hasTemplatePlaceholder(value) {
|
|
1091
|
-
return /\$\{|%7B|%7D/i.test(value);
|
|
1092
|
-
}
|
|
1093
|
-
function urlTargetFromExpression(expr, use) {
|
|
1094
|
-
const resolved2 = resolveExpression(expr, use, "external");
|
|
1095
|
-
if (resolved2.status === "static" && resolved2.value && !hasTemplatePlaceholder(resolved2.value)) return { kind: "static_url", expression: resolved2.value, dynamic: false, sourceKind: resolved2.sourceKind };
|
|
1096
|
-
if (expr) return { kind: "url_expression", dynamic: true, expression: `${resolved2.sourceKind}:${resolved2.placeholderKeys.join("|")}`, expressionShape: resolved2.sourceKind, placeholderKeys: resolved2.placeholderKeys };
|
|
1097
|
-
return { kind: "unknown", dynamic: false };
|
|
1098
|
-
}
|
|
1099
|
-
function destinationTargetFromExpression(expr, use) {
|
|
1100
|
-
const resolved2 = resolveExpression(expr, use, "external");
|
|
1101
|
-
const text = resolved2.value;
|
|
1102
|
-
if (resolved2.status === "static" && text && !hasTemplatePlaceholder(text)) return { kind: "destination", expression: text, dynamic: false, sourceKind: resolved2.sourceKind };
|
|
1103
|
-
const candidates = staticConditionalCandidates(expr, /* @__PURE__ */ new Map());
|
|
1104
|
-
if (candidates) return { kind: "destination", dynamic: true, expressionShape: "conditional", candidateLiterals: candidates };
|
|
1105
|
-
const shape = destinationExpressionShape(expr);
|
|
1106
|
-
if (shape) return { kind: "destination", dynamic: true, expressionShape: shape };
|
|
1107
|
-
return void 0;
|
|
1108
|
-
}
|
|
1109
|
-
function externalHttpEvidence(node, source) {
|
|
1110
|
-
const expr = node.expression;
|
|
1111
|
-
const exprText = expr.getText(source);
|
|
1112
|
-
if (exprText === "useOrFetchDestination") {
|
|
1113
|
-
const objectArg = node.arguments[0];
|
|
1114
|
-
if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
|
|
1115
|
-
const destination = destinationTargetFromExpression(propertyInitializer(objectArg, "destinationName"), node);
|
|
1116
|
-
return { externalTarget: destination ?? { kind: "unknown", dynamic: false }, classifier: "sap_destination_lookup", sourceCallShape: "useOrFetchDestination" };
|
|
1117
|
-
}
|
|
973
|
+
async function recordVariable(decl) {
|
|
974
|
+
if (!ts5.isIdentifier(decl.name) || !decl.initializer) return;
|
|
975
|
+
await recordBindingFromExpression(decl.name.text, decl.initializer, decl, "declaration");
|
|
1118
976
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
const
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
return { method, externalTarget: destination ? { ...url, destination } : url, classifier: "sap_execute_http_request", sourceCallShape: "executeHttpRequest" };
|
|
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];
|
|
1125
982
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
return { externalTarget: { kind: "unknown", dynamic: false }, classifier: "axios_unknown_call", sourceCallShape: "axios(...)" };
|
|
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);
|
|
1133
989
|
}
|
|
1134
|
-
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
|
|
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;
|
|
1138
1022
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
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
|
+
});
|
|
1047
|
+
}
|
|
1141
1048
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
function receiverName(expr) {
|
|
1157
|
-
if (ts4.isIdentifier(expr)) return expr.text;
|
|
1158
|
-
if (ts4.isPropertyAccessExpression(expr)) return expr.getText(sourceOf(expr));
|
|
1159
|
-
return void 0;
|
|
1160
|
-
}
|
|
1161
|
-
function sourceOf(node) {
|
|
1162
|
-
return node.getSourceFile();
|
|
1163
|
-
}
|
|
1164
|
-
function rootReceiverName(expr) {
|
|
1165
|
-
if (ts4.isIdentifier(expr)) return expr.text;
|
|
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);
|
|
1187
|
-
}
|
|
1188
|
-
ts4.forEachChild(node, collectCalls);
|
|
1189
|
-
};
|
|
1190
|
-
collectCalls(source);
|
|
1191
|
-
const scanFunction = (name, fn) => {
|
|
1192
|
-
if (!calledNames.has(name)) return;
|
|
1193
|
-
const params = fn.parameters.map((param) => ts4.isIdentifier(param.name) ? param.name.text : void 0);
|
|
1194
|
-
const sends = [];
|
|
1195
|
-
const visit = (node) => {
|
|
1196
|
-
if (ts4.isCallExpression(node) && ts4.isPropertyAccessExpression(node.expression) && node.expression.name.text === "send" && ts4.isIdentifier(node.expression.expression)) {
|
|
1197
|
-
const objectArg = node.arguments[0];
|
|
1198
|
-
if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
|
|
1199
|
-
const pathProp = propertyInitializer(objectArg, "path");
|
|
1200
|
-
const methodProp = propertyInitializer(objectArg, "method");
|
|
1201
|
-
const pathName = pathProp && ts4.isIdentifier(pathProp) ? pathProp.text : void 0;
|
|
1202
|
-
const methodName = methodProp && ts4.isIdentifier(methodProp) ? methodProp.text : void 0;
|
|
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
|
-
if (ts4.isCallExpression(node) && ts4.isIdentifier(node.expression) && specs.has(node.expression.text)) {
|
|
1208
|
-
const nested = specs.get(node.expression.text);
|
|
1209
|
-
const pathArg = nested ? node.arguments[nested.pathIndex] : void 0;
|
|
1210
|
-
const clientArg = nested?.clientIndex === void 0 ? void 0 : node.arguments[nested.clientIndex];
|
|
1211
|
-
const pathName = pathArg && ts4.isIdentifier(pathArg) ? pathArg.text : void 0;
|
|
1212
|
-
const clientName = clientArg && ts4.isIdentifier(clientArg) ? clientArg.text : nested?.clientName;
|
|
1213
|
-
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() });
|
|
1214
|
-
}
|
|
1215
|
-
ts4.forEachChild(node, visit);
|
|
1216
|
-
};
|
|
1217
|
-
visit(fn);
|
|
1218
|
-
if (sends.length !== 1) return;
|
|
1219
|
-
const found = sends[0];
|
|
1220
|
-
const clientIndex = params.indexOf(found.client);
|
|
1221
|
-
const pathIndex = params.indexOf(found.path);
|
|
1222
|
-
const methodIndex = found.method ? params.indexOf(found.method) : -1;
|
|
1223
|
-
const capturesKnownClient = serviceVariables.has(found.client) || /^(srv|service|serviceClient|client|.*Client)$/.test(found.client);
|
|
1224
|
-
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: lineOf3(source.text, fn.getStart(source)), internalStart: found.start, internalEnd: found.end });
|
|
1225
|
-
};
|
|
1226
|
-
const visitTop = (node) => {
|
|
1227
|
-
if (ts4.isFunctionDeclaration(node) && node.name) scanFunction(node.name.text, node);
|
|
1228
|
-
if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.initializer && (ts4.isArrowFunction(node.initializer) || ts4.isFunctionExpression(node.initializer))) scanFunction(node.name.text, node.initializer);
|
|
1229
|
-
ts4.forEachChild(node, visitTop);
|
|
1230
|
-
};
|
|
1231
|
-
visitTop(source);
|
|
1232
|
-
return specs;
|
|
1233
|
-
}
|
|
1234
|
-
function classifyOutboundCallsInSource(source, filePath) {
|
|
1235
|
-
const calls = [];
|
|
1236
|
-
const sourceFile = normalizePath(filePath);
|
|
1237
|
-
const initializers = variableInitializers(source);
|
|
1238
|
-
const serviceVariables = collectServiceVariables(source);
|
|
1239
|
-
const wrapperSpecs = collectWrapperSpecs(source);
|
|
1240
|
-
const wrapperInternalRanges = [...wrapperSpecs.values()].map((spec) => ({ start: spec.internalStart, end: spec.internalEnd }));
|
|
1241
|
-
const add = (node, fact, extra) => {
|
|
1242
|
-
calls.push({ node, fact: { ...fact, sourceFile, sourceLine: lineOf3(source.text, node.getStart(source)), confidence: fact.confidence ?? 0.8, evidence: parserEvidence(source, node, extra) } });
|
|
1243
|
-
};
|
|
1244
|
-
const visit = (node) => {
|
|
1245
|
-
if (ts4.isCallExpression(node)) {
|
|
1246
|
-
if (wrapperInternalRanges.some((range) => node.getStart(source) >= range.start && node.getEnd() <= range.end)) {
|
|
1247
|
-
return;
|
|
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;
|
|
1248
1063
|
}
|
|
1249
|
-
|
|
1250
|
-
const
|
|
1251
|
-
if (
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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
|
+
});
|
|
1080
|
+
}
|
|
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,
|
|
1098
|
+
sourceFile: normalizePath(filePath),
|
|
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
|
|
1289
1109
|
}
|
|
1290
|
-
|
|
1291
|
-
}
|
|
1292
|
-
const wrapperName = ts4.isIdentifier(expr) ? expr.text : ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) ? expr.expression.text : "";
|
|
1293
|
-
const wrapperArgs = ts4.isIdentifier(expr) ? node.arguments : ts4.isCallExpression(expr) ? expr.arguments : node.arguments;
|
|
1294
|
-
const spec = wrapperSpecs.get(wrapperName);
|
|
1295
|
-
const clientArg = spec?.clientIndex === void 0 ? void 0 : wrapperArgs[spec.clientIndex];
|
|
1296
|
-
const pathArg = spec ? wrapperArgs[spec.pathIndex] : void 0;
|
|
1297
|
-
const methodArg = spec?.methodIndex === void 0 ? void 0 : wrapperArgs[spec.methodIndex];
|
|
1298
|
-
const receiver = clientArg && ts4.isIdentifier(clientArg) ? clientArg.text : spec?.clientName;
|
|
1299
|
-
const method = stripQuotes(resolveExpression(methodArg, node, "literal").value ?? spec?.methodLiteral ?? "POST");
|
|
1300
|
-
const resolvedPath = staticPathExpression(pathArg, node);
|
|
1301
|
-
const operationPathExpr = operationPathFromStatic(resolvedPath.text);
|
|
1302
|
-
const normalizedOperationPath = operationPathExpr ? classifyODataPathIntent(operationPathExpr, method).topLevelOperationName : void 0;
|
|
1303
|
-
if (spec && receiver && operationPathExpr) {
|
|
1304
|
-
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: 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 });
|
|
1305
|
-
} else if (spec && receiver) {
|
|
1306
|
-
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" });
|
|
1307
|
-
}
|
|
1308
|
-
} else if (ts4.isPropertyAccessExpression(expr) && ["emit", "publish", "on"].includes(expr.name.text)) {
|
|
1309
|
-
const receiver = receiverName(expr.expression);
|
|
1310
|
-
const rootReceiver = rootReceiverName(expr.expression);
|
|
1311
|
-
if (isSupportedEventReceiver(receiver, rootReceiver, serviceVariables)) {
|
|
1312
|
-
const eventName = literalText(node.arguments[0]);
|
|
1313
|
-
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" });
|
|
1314
|
-
}
|
|
1315
|
-
} else {
|
|
1316
|
-
const external = externalHttpEvidence(node, source);
|
|
1317
|
-
if (external) {
|
|
1318
|
-
const evidenceTarget = { ...external.externalTarget, method: external.method, parserClassifier: external.classifier, sourceCallShape: external.sourceCallShape };
|
|
1319
|
-
const safeTarget = externalHttpTarget({ method: external.method, evidence_json: JSON.stringify({ externalTarget: evidenceTarget }) });
|
|
1320
|
-
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 });
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1110
|
+
]
|
|
1111
|
+
});
|
|
1323
1112
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
if (origin) aliases.set(node.name.text, { ...origin, chain: [...origin.chain, node.name.text] });
|
|
1113
|
+
}
|
|
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 };
|
|
1125
|
+
}
|
|
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;
|
|
1348
1136
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
unresolvedReason: ["send", "emit", "publish", "on"].includes(parsed.operation) ? "transport_client_method" : void 0,
|
|
1362
|
-
evidence: parserEvidence(source, node, {
|
|
1363
|
-
classifier: parsed.classifier,
|
|
1364
|
-
parserCallType: parsed.operation === "send" ? "transport_client_method" : parsed.classifier,
|
|
1365
|
-
localServiceLookup: parsed.lookup,
|
|
1366
|
-
localServiceName: parsed.service,
|
|
1367
|
-
operation: parsed.operation,
|
|
1368
|
-
aliasChain: parsed.chain
|
|
1369
|
-
})
|
|
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
|
+
]
|
|
1370
1149
|
});
|
|
1371
1150
|
}
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1151
|
+
}
|
|
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
|
+
}
|
|
1164
|
+
}
|
|
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);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
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);
|
|
1182
|
+
}
|
|
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);
|
|
1198
|
+
}
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
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);
|
|
1217
|
+
}
|
|
1218
|
+
return out;
|
|
1382
1219
|
}
|
|
1383
|
-
function
|
|
1384
|
-
const
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
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);
|
|
1229
|
+
}
|
|
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);
|
|
1271
|
+
}
|
|
1394
1272
|
}
|
|
1395
|
-
return
|
|
1273
|
+
return helpers;
|
|
1396
1274
|
}
|
|
1397
1275
|
|
|
1398
|
-
// src/parsers/
|
|
1399
|
-
import path9 from "path";
|
|
1400
|
-
import ts6 from "typescript";
|
|
1401
|
-
|
|
1402
|
-
// src/parsers/service-binding-parser-helpers.ts
|
|
1276
|
+
// src/parsers/outbound-call-parser.ts
|
|
1403
1277
|
import fs7 from "fs/promises";
|
|
1404
|
-
import
|
|
1405
|
-
import
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
if (ts5.isTemplateExpression(node)) return node.getText().replace(/^`|`$/g, "");
|
|
1413
|
-
return node.getText();
|
|
1414
|
-
}
|
|
1415
|
-
function placeholders2(value) {
|
|
1416
|
-
return [...(value ?? "").matchAll(/\$\{([^}]*)\}/g)].map((m) => (m[1] ?? "").trim()).filter(Boolean);
|
|
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);
|
|
1417
1286
|
}
|
|
1418
|
-
function
|
|
1419
|
-
|
|
1420
|
-
if (!ts5.isPropertyAccessExpression(expr) || expr.name.text !== "to") return void 0;
|
|
1421
|
-
const inner = expr.expression;
|
|
1422
|
-
if (!ts5.isPropertyAccessExpression(inner) || inner.name.text !== "connect" || inner.expression.getText() !== "cds") return void 0;
|
|
1423
|
-
const first = call.arguments[0];
|
|
1424
|
-
if (!first) return void 0;
|
|
1425
|
-
const second = call.arguments[1];
|
|
1426
|
-
const objectArg = ts5.isObjectLiteralExpression(first) ? first : second && ts5.isObjectLiteralExpression(second) ? second : void 0;
|
|
1427
|
-
let alias;
|
|
1428
|
-
let aliasExpr;
|
|
1429
|
-
if (ts5.isStringLiteralLike(first) || ts5.isNoSubstitutionTemplateLiteral(first)) alias = first.text;
|
|
1430
|
-
else if (!ts5.isObjectLiteralExpression(first)) aliasExpr = stringValue(first);
|
|
1431
|
-
if ((ts5.isStringLiteralLike(first) || ts5.isNoSubstitutionTemplateLiteral(first)) && !objectArg) return { alias: first.text, isDynamic: false, placeholders: [] };
|
|
1432
|
-
if (!objectArg && aliasExpr) return { aliasExpr, isDynamic: true, placeholders: placeholders2(aliasExpr) };
|
|
1433
|
-
const expressions = objectArg ? objectExpressions(objectArg) : {};
|
|
1434
|
-
const ph = [...placeholders2(aliasExpr ?? alias), ...placeholders2(expressions.destinationExpr), ...placeholders2(expressions.servicePathExpr)];
|
|
1435
|
-
return { alias, aliasExpr, ...expressions, isDynamic: ph.length > 0 || !expressions.destinationExpr && !expressions.servicePathExpr, placeholders: ph };
|
|
1287
|
+
function methodPrefix(method) {
|
|
1288
|
+
return typeof method === "string" && method.length > 0 ? `${method.toUpperCase()} ` : "";
|
|
1436
1289
|
}
|
|
1437
|
-
function
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
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>");
|
|
1447
1300
|
}
|
|
1448
|
-
visitObject(objectArg);
|
|
1449
|
-
return out;
|
|
1450
|
-
}
|
|
1451
|
-
function unwrapCall(expr) {
|
|
1452
|
-
if (ts5.isAwaitExpression(expr)) return unwrapCall(expr.expression);
|
|
1453
|
-
if (ts5.isParenthesizedExpression(expr)) return unwrapCall(expr.expression);
|
|
1454
|
-
if (ts5.isAsExpression(expr) || ts5.isSatisfiesExpression(expr)) return unwrapCall(expr.expression);
|
|
1455
|
-
if (ts5.isTypeAssertionExpression(expr)) return unwrapCall(expr.expression);
|
|
1456
|
-
if (ts5.isCallExpression(expr)) return expr;
|
|
1457
|
-
return void 0;
|
|
1458
|
-
}
|
|
1459
|
-
function unwrapIdentityExpression(expr) {
|
|
1460
|
-
if (ts5.isAwaitExpression(expr)) return unwrapIdentityExpression(expr.expression);
|
|
1461
|
-
if (ts5.isParenthesizedExpression(expr)) return unwrapIdentityExpression(expr.expression);
|
|
1462
|
-
if (ts5.isAsExpression(expr) || ts5.isSatisfiesExpression(expr)) return unwrapIdentityExpression(expr.expression);
|
|
1463
|
-
if (ts5.isTypeAssertionExpression(expr)) return unwrapIdentityExpression(expr.expression);
|
|
1464
|
-
return expr;
|
|
1465
|
-
}
|
|
1466
|
-
function transactionReceiverName(expr) {
|
|
1467
|
-
const call = unwrapCall(expr);
|
|
1468
|
-
if (call && ts5.isPropertyAccessExpression(call.expression) && ["tx", "transaction"].includes(call.expression.name.text) && ts5.isIdentifier(call.expression.expression)) return call.expression.expression.text;
|
|
1469
|
-
const unwrapped = unwrapIdentityExpression(expr);
|
|
1470
|
-
if (!ts5.isConditionalExpression(unwrapped)) return void 0;
|
|
1471
|
-
const left = transactionReceiverName(unwrapped.whenTrue);
|
|
1472
|
-
const right = transactionReceiverName(unwrapped.whenFalse);
|
|
1473
|
-
return left && left === right ? left : void 0;
|
|
1474
1301
|
}
|
|
1475
|
-
function
|
|
1476
|
-
const
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
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}` };
|
|
1480
1312
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
if (!found) ts5.forEachChild(node, visit);
|
|
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 };
|
|
1486
1317
|
}
|
|
1487
|
-
|
|
1488
|
-
return
|
|
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 };
|
|
1489
1320
|
}
|
|
1490
|
-
|
|
1321
|
+
function safeParse(value) {
|
|
1491
1322
|
try {
|
|
1492
|
-
const
|
|
1493
|
-
return
|
|
1323
|
+
const parsed = JSON.parse(value);
|
|
1324
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1494
1325
|
} catch {
|
|
1495
|
-
return
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
async function resolveImport(repoPath, fromFile, spec) {
|
|
1499
|
-
if (!spec.startsWith(".")) return void 0;
|
|
1500
|
-
const rawBase = path8.resolve(repoPath, path8.dirname(fromFile), spec);
|
|
1501
|
-
const parsed = path8.parse(rawBase);
|
|
1502
|
-
const base = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"].includes(parsed.ext) ? path8.join(parsed.dir, parsed.name) : rawBase;
|
|
1503
|
-
for (const candidate of [base, `${base}.ts`, `${base}.js`, path8.join(base, "index.ts"), path8.join(base, "index.js")]) {
|
|
1504
|
-
const stat = await fs7.stat(candidate).catch(() => void 0);
|
|
1505
|
-
if (stat?.isFile()) return normalizePath(path8.relative(repoPath, candidate));
|
|
1506
|
-
}
|
|
1507
|
-
return void 0;
|
|
1508
|
-
}
|
|
1509
|
-
async function importsFor(repoPath, filePath, sf) {
|
|
1510
|
-
const imports = [];
|
|
1511
|
-
for (const stmt of sf.statements) {
|
|
1512
|
-
if (!ts5.isImportDeclaration(stmt) || !ts5.isStringLiteralLike(stmt.moduleSpecifier)) continue;
|
|
1513
|
-
const sourceFile = await resolveImport(repoPath, filePath, stmt.moduleSpecifier.text);
|
|
1514
|
-
const clause = stmt.importClause;
|
|
1515
|
-
if (!clause) continue;
|
|
1516
|
-
if (clause.name) imports.push({ localName: clause.name.text, exportedName: "default", sourceFile });
|
|
1517
|
-
const bindings = clause.namedBindings;
|
|
1518
|
-
if (bindings && ts5.isNamedImports(bindings))
|
|
1519
|
-
for (const el of bindings.elements) imports.push({ localName: el.name.text, exportedName: el.propertyName?.text ?? el.name.text, sourceFile });
|
|
1326
|
+
return {};
|
|
1520
1327
|
}
|
|
1521
|
-
return imports;
|
|
1522
1328
|
}
|
|
1523
1329
|
|
|
1524
|
-
// src/
|
|
1525
|
-
function
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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
|
+
};
|
|
1543
1355
|
}
|
|
1544
|
-
function
|
|
1545
|
-
const
|
|
1546
|
-
const
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
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" };
|
|
1560
1391
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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" };
|
|
1396
|
+
}
|
|
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" };
|
|
1566
1401
|
}
|
|
1567
|
-
return
|
|
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" };
|
|
1568
1404
|
}
|
|
1569
|
-
function
|
|
1570
|
-
|
|
1571
|
-
if (
|
|
1572
|
-
|
|
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;
|
|
1573
1411
|
}
|
|
1574
|
-
function
|
|
1575
|
-
|
|
1576
|
-
let returned;
|
|
1577
|
-
function visit(node) {
|
|
1578
|
-
if (node !== fn && (ts6.isFunctionDeclaration(node) || ts6.isArrowFunction(node) || ts6.isFunctionExpression(node)))
|
|
1579
|
-
return;
|
|
1580
|
-
if (!returned && ts6.isReturnStatement(node) && node.expression)
|
|
1581
|
-
returned = node.expression;
|
|
1582
|
-
if (!returned) ts6.forEachChild(node, visit);
|
|
1583
|
-
}
|
|
1584
|
-
visit(fn);
|
|
1585
|
-
if (!returned) return void 0;
|
|
1586
|
-
if (ts6.isIdentifier(returned)) return localBindings.get(returned.text);
|
|
1587
|
-
return findConnectInExpression(returned);
|
|
1412
|
+
function isMediaOrPropertySuffix(segment) {
|
|
1413
|
+
return ["file", "content", "$value", "metadata", "items"].includes(segment.toLowerCase());
|
|
1588
1414
|
}
|
|
1589
|
-
function
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
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);
|
|
1593
1420
|
}
|
|
1594
|
-
function
|
|
1595
|
-
const
|
|
1596
|
-
for (
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
for (const decl of stmt.declarationList.declarations)
|
|
1604
|
-
if (ts6.isIdentifier(decl.name)) exports.set(decl.name.text, decl.name.text);
|
|
1605
|
-
}
|
|
1606
|
-
if (!ts6.isExportDeclaration(stmt) || !stmt.exportClause) continue;
|
|
1607
|
-
if (!ts6.isNamedExports(stmt.exportClause)) continue;
|
|
1608
|
-
for (const el of stmt.exportClause.elements)
|
|
1609
|
-
exports.set(el.name.text, el.propertyName?.text ?? el.name.text);
|
|
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;
|
|
1610
1430
|
}
|
|
1611
|
-
return
|
|
1431
|
+
return keys;
|
|
1612
1432
|
}
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
}
|
|
1627
|
-
if (ts6.isVariableStatement(stmt))
|
|
1628
|
-
for (const decl of stmt.declarationList.declarations) {
|
|
1629
|
-
if (!ts6.isIdentifier(decl.name) || !decl.initializer) continue;
|
|
1630
|
-
const helper = functionLikeInitializer(decl.initializer);
|
|
1631
|
-
if (helper) {
|
|
1632
|
-
const directReturn = directConnectFactFromFunctionLike(helper);
|
|
1633
|
-
if (directReturn)
|
|
1634
|
-
factsByLocal.set(decl.name.text, {
|
|
1635
|
-
...directReturn,
|
|
1636
|
-
sourceLine: lineOf4(sourceFileAst, decl)
|
|
1637
|
-
});
|
|
1638
|
-
for (const [returnedProperty, objectFact] of collectReturnedObjectBindings(helper))
|
|
1639
|
-
factsByLocal.set(`${decl.name.text}#${returnedProperty}`, {
|
|
1640
|
-
...objectFact,
|
|
1641
|
-
returnedProperty,
|
|
1642
|
-
sourceLine: lineOf4(sourceFileAst, decl)
|
|
1643
|
-
});
|
|
1644
|
-
continue;
|
|
1645
|
-
}
|
|
1646
|
-
const fact = findConnectInExpression(decl.initializer);
|
|
1647
|
-
if (fact)
|
|
1648
|
-
factsByLocal.set(decl.name.text, {
|
|
1649
|
-
...fact,
|
|
1650
|
-
sourceLine: lineOf4(sourceFileAst, decl)
|
|
1651
|
-
});
|
|
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;
|
|
1652
1446
|
}
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
if (
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
if (
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
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;
|
|
1670
1473
|
}
|
|
1671
1474
|
}
|
|
1672
|
-
return
|
|
1475
|
+
return void 0;
|
|
1673
1476
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
if (ts6.isFunctionDeclaration(stmt) && stmt.name) {
|
|
1687
|
-
const directFact = directConnectFactFromFunctionLike(stmt);
|
|
1688
|
-
if (directFact) localDirectHelpers.set(stmt.name.text, { ...directFact, exportedName: stmt.name.text, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, stmt) });
|
|
1689
|
-
const rows2 = [];
|
|
1690
|
-
for (const [returnedProperty, fact] of collectReturnedObjectBindings(stmt))
|
|
1691
|
-
rows2.push({ ...fact, exportedName: stmt.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, stmt) });
|
|
1692
|
-
if (rows2.length > 0) localObjectHelpers.set(stmt.name.text, rows2);
|
|
1693
|
-
}
|
|
1694
|
-
if (ts6.isVariableStatement(stmt)) {
|
|
1695
|
-
for (const decl of stmt.declarationList.declarations) {
|
|
1696
|
-
if (!ts6.isIdentifier(decl.name)) continue;
|
|
1697
|
-
const helper = functionLikeInitializer(decl.initializer);
|
|
1698
|
-
if (!helper) continue;
|
|
1699
|
-
const directFact = directConnectFactFromFunctionLike(helper);
|
|
1700
|
-
if (directFact) localDirectHelpers.set(decl.name.text, { ...directFact, exportedName: decl.name.text, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, decl) });
|
|
1701
|
-
const rows2 = [];
|
|
1702
|
-
for (const [returnedProperty, fact] of collectReturnedObjectBindings(helper))
|
|
1703
|
-
rows2.push({ ...fact, exportedName: decl.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, decl) });
|
|
1704
|
-
if (rows2.length > 0) localObjectHelpers.set(decl.name.text, rows2);
|
|
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;
|
|
1705
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;
|
|
1706
1510
|
}
|
|
1707
1511
|
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
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;
|
|
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;
|
|
1743
1549
|
}
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
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);
|
|
1749
1569
|
}
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
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 };
|
|
1789
1680
|
}
|
|
1790
1681
|
}
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
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
|
+
}
|
|
1800
1703
|
}
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
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;
|
|
1807
1711
|
}
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
callerVariable: targetName,
|
|
1829
|
-
sourceVariable: unwrapped.expression.text,
|
|
1830
|
-
returnedProperty: unwrapped.name.text,
|
|
1831
|
-
assignedFromProperty: unwrapped.getText(sourceFileAst),
|
|
1832
|
-
importSource: resolved2.imp?.sourceFile,
|
|
1833
|
-
exportedSymbol: resolved2.imp?.exportedName,
|
|
1834
|
-
helperSourceFile: resolved2.helper.sourceFile,
|
|
1835
|
-
helperSourceLine: resolved2.helper.sourceLine
|
|
1836
|
-
}
|
|
1837
|
-
]
|
|
1838
|
-
});
|
|
1839
|
-
return true;
|
|
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;
|
|
1840
1732
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
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);
|
|
1865
1770
|
}
|
|
1866
1771
|
}
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
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;
|
|
1899
1852
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
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"] };
|
|
1931
1894
|
}
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
if (
|
|
1937
|
-
|
|
1938
|
-
const
|
|
1939
|
-
|
|
1940
|
-
const container = unwrapIdentityExpression(first);
|
|
1941
|
-
if (!ts6.isArrayLiteralExpression(container)) return void 0;
|
|
1942
|
-
return { elements: container.elements, promiseAll: true };
|
|
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] };
|
|
1943
1903
|
}
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
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);
|
|
1968
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;
|
|
1969
1970
|
}
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
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" };
|
|
1981
2009
|
}
|
|
1982
2010
|
}
|
|
1983
|
-
|
|
1984
|
-
const
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
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" };
|
|
2017
|
+
}
|
|
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)" };
|
|
1992
2023
|
}
|
|
2024
|
+
return { externalTarget: { kind: "unknown", dynamic: false }, classifier: "axios_unknown_call", sourceCallShape: "axios(...)" };
|
|
1993
2025
|
}
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
events.push({ pos: node.getStart(sourceFileAst), node });
|
|
1999
|
-
ts6.forEachChild(node, collectEvents);
|
|
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" };
|
|
2000
2030
|
}
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
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}` };
|
|
2033
|
+
}
|
|
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);
|
|
2042
|
+
}
|
|
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);
|
|
2079
|
+
}
|
|
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
|
+
}
|
|
2016
2098
|
}
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
continue;
|
|
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() });
|
|
2025
2106
|
}
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
const
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
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;
|
|
2037
2125
|
}
|
|
2038
|
-
function
|
|
2039
|
-
const
|
|
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
|
-
|
|
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" });
|
|
2075
2181
|
}
|
|
2076
2182
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
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
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
ts7.forEachChild(node, visit);
|
|
2217
|
+
};
|
|
2218
|
+
visit(source);
|
|
2219
|
+
return calls;
|
|
2220
|
+
}
|
|
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
|
+
});
|
|
2089
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" };
|
|
2090
2288
|
}
|
|
2091
|
-
return
|
|
2289
|
+
return { ...origin, operation: expr.name.text, classifier: "local_cap_service_call" };
|
|
2092
2290
|
}
|
|
2093
2291
|
|
|
2094
2292
|
// src/linker/dynamic-edge-resolver.ts
|
|
@@ -2118,6 +2316,171 @@ function substituteVariables(template, vars) {
|
|
|
2118
2316
|
};
|
|
2119
2317
|
}
|
|
2120
2318
|
|
|
2319
|
+
// src/trace/implementation-hints.ts
|
|
2320
|
+
function parseImplementationHint(value) {
|
|
2321
|
+
const hint = {};
|
|
2322
|
+
for (const part of value.split(",")) {
|
|
2323
|
+
const separator = part.indexOf("=");
|
|
2324
|
+
if (separator <= 0 || separator === part.length - 1) throw new Error(`Invalid implementation hint field: ${part}`);
|
|
2325
|
+
assignHintField(hint, part.slice(0, separator).trim(), part.slice(separator + 1).trim());
|
|
2326
|
+
}
|
|
2327
|
+
if (!hint.implementationRepo) throw new Error("Scoped implementation hint requires an implementation repo selection");
|
|
2328
|
+
return { ...hint, implementationRepo: hint.implementationRepo };
|
|
2329
|
+
}
|
|
2330
|
+
function selectImplementation(rawEvidence, hints, legacyRepo) {
|
|
2331
|
+
const evidence = asEvidence(rawEvidence);
|
|
2332
|
+
const scoped = hints ?? [];
|
|
2333
|
+
const matchingHints = scoped.filter((hint2) => hintMatchesEdge(hint2, evidence));
|
|
2334
|
+
if (matchingHints.length === 0) {
|
|
2335
|
+
if (legacyRepo) return selectCandidate(evidence, legacyHint(legacyRepo), "implementation_repo_hint");
|
|
2336
|
+
const reason = scoped.length > 0 ? "no_scoped_hint_matched_edge" : "no_implementation_hint_supplied";
|
|
2337
|
+
return { blocksAutomatic: false, evidence: { status: "not_matched", reason, strategy: "scoped_implementation_hint" } };
|
|
2338
|
+
}
|
|
2339
|
+
if (matchingHints.length > 1) {
|
|
2340
|
+
return {
|
|
2341
|
+
blocksAutomatic: true,
|
|
2342
|
+
evidence: {
|
|
2343
|
+
status: "tied",
|
|
2344
|
+
reason: "multiple_scoped_hints_matched_edge",
|
|
2345
|
+
strategy: "scoped_implementation_hint",
|
|
2346
|
+
matchedHints: matchingHints,
|
|
2347
|
+
candidateCount: matchingHints.length
|
|
2348
|
+
}
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
const hint = matchingHints[0];
|
|
2352
|
+
return hint ? selectCandidate(evidence, hint, "scoped_implementation_hint") : { blocksAutomatic: false, evidence: { status: "not_matched" } };
|
|
2353
|
+
}
|
|
2354
|
+
function implementationHintDiagnostic(selection, suggestions) {
|
|
2355
|
+
if (!selection.blocksAutomatic || selection.methodId) return void 0;
|
|
2356
|
+
return {
|
|
2357
|
+
severity: "warning",
|
|
2358
|
+
code: "implementation_hint_mismatch",
|
|
2359
|
+
message: "Implementation hint did not select exactly one viable candidate",
|
|
2360
|
+
hintStatus: selection.evidence.status,
|
|
2361
|
+
candidateCount: selection.evidence.candidateCount,
|
|
2362
|
+
implementationHintSuggestions: Array.isArray(suggestions) && suggestions.length > 0 ? suggestions : void 0,
|
|
2363
|
+
implementationSelection: selection.evidence
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
function implementationHintSuggestions(rawEvidence) {
|
|
2367
|
+
const evidence = asEvidence(rawEvidence);
|
|
2368
|
+
const accepted = (evidence.candidates ?? []).filter((candidate) => candidate.accepted);
|
|
2369
|
+
if (accepted.length < 2) return [];
|
|
2370
|
+
const repos = selectableRepositories(accepted);
|
|
2371
|
+
return accepted.flatMap((candidate) => {
|
|
2372
|
+
const repo = candidate.handlerPackage?.name;
|
|
2373
|
+
if (!repo || !repos.includes(repo)) return [];
|
|
2374
|
+
const hint = suggestionHint(evidence, candidate, repo);
|
|
2375
|
+
return [{
|
|
2376
|
+
servicePath: hint.servicePath,
|
|
2377
|
+
operationPath: hint.operationPath,
|
|
2378
|
+
ambiguityReason: evidence.ambiguityReasons?.[0],
|
|
2379
|
+
candidateFamily: hint.candidateFamily,
|
|
2380
|
+
selectableImplementationRepositories: repos,
|
|
2381
|
+
implementationRepo: repo,
|
|
2382
|
+
hint,
|
|
2383
|
+
cli: `--implementation-hint ${hintString(hint)}`
|
|
2384
|
+
}];
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
function selectableRepositories(candidates) {
|
|
2388
|
+
const repos = new Set(candidates.flatMap((candidate) => candidate.handlerPackage?.name ? [candidate.handlerPackage.name] : []));
|
|
2389
|
+
return [...repos].filter((repo) => candidates.filter((candidate) => candidateMatchesRepo(candidate, repo)).length === 1).sort();
|
|
2390
|
+
}
|
|
2391
|
+
function assignHintField(hint, key, value) {
|
|
2392
|
+
if (key === "service" || key === "servicePath") hint.servicePath = value;
|
|
2393
|
+
else if (key === "operation" || key === "operationPath") hint.operationPath = value;
|
|
2394
|
+
else if (key === "package" || key === "packageName") hint.packageName = value;
|
|
2395
|
+
else if (key === "repository" || key === "repositoryName") hint.repositoryName = value;
|
|
2396
|
+
else if (key === "family" || key === "candidateFamily") hint.candidateFamily = value;
|
|
2397
|
+
else if (key === "repo" || key === "implementationRepo" || key === "select") hint.implementationRepo = value;
|
|
2398
|
+
else throw new Error(`Unknown implementation hint field: ${key}`);
|
|
2399
|
+
}
|
|
2400
|
+
function selectCandidate(evidence, hint, strategy) {
|
|
2401
|
+
const matches2 = (evidence.candidates ?? []).filter((candidate) => candidate.accepted && candidateMatchesRepo(candidate, hint.implementationRepo));
|
|
2402
|
+
const selected = matches2.length === 1 ? matches2[0] : void 0;
|
|
2403
|
+
if (!selected || selected.methodId === void 0) {
|
|
2404
|
+
return {
|
|
2405
|
+
blocksAutomatic: true,
|
|
2406
|
+
evidence: {
|
|
2407
|
+
status: matches2.length > 1 ? "tied" : "not_matched",
|
|
2408
|
+
reason: matches2.length > 1 ? "hint_matched_multiple_candidates" : "hint_matched_zero_candidates",
|
|
2409
|
+
strategy,
|
|
2410
|
+
matchedHint: hint,
|
|
2411
|
+
selectedRepo: hint.implementationRepo,
|
|
2412
|
+
candidateCount: matches2.length
|
|
2413
|
+
}
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
return {
|
|
2417
|
+
methodId: String(selected.methodId),
|
|
2418
|
+
blocksAutomatic: false,
|
|
2419
|
+
evidence: {
|
|
2420
|
+
status: "selected",
|
|
2421
|
+
guided: true,
|
|
2422
|
+
strategy,
|
|
2423
|
+
matchedHint: hint,
|
|
2424
|
+
selectedRepo: hint.implementationRepo,
|
|
2425
|
+
selectedMethodId: selected.methodId,
|
|
2426
|
+
ambiguityReason: evidence.ambiguityReasons?.[0]
|
|
2427
|
+
}
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
function suggestionHint(evidence, candidate, repo) {
|
|
2431
|
+
const servicePath = evidence.servicePath ?? candidate.servicePath;
|
|
2432
|
+
const operationPath = evidence.operationPath ?? candidate.operationPath;
|
|
2433
|
+
const family = usefulCandidateFamily(evidence, candidate);
|
|
2434
|
+
return {
|
|
2435
|
+
...servicePath ? { servicePath } : {},
|
|
2436
|
+
...operationPath ? { operationPath } : {},
|
|
2437
|
+
...evidence.modelPackage?.packageName ? { packageName: evidence.modelPackage.packageName } : {},
|
|
2438
|
+
...evidence.modelPackage?.name ? { repositoryName: evidence.modelPackage.name } : {},
|
|
2439
|
+
...family ? { candidateFamily: family } : {},
|
|
2440
|
+
implementationRepo: repo
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
function usefulCandidateFamily(evidence, candidate) {
|
|
2444
|
+
const family = candidate.handlerPackage?.packageName;
|
|
2445
|
+
if (!family) return void 0;
|
|
2446
|
+
if ((evidence.candidateFamilies ?? []).some((item) => item.packageName === family)) return family;
|
|
2447
|
+
const acceptedFamilies = new Set(
|
|
2448
|
+
(evidence.candidates ?? []).filter((item) => item.accepted).flatMap((item) => item.handlerPackage?.packageName ? [item.handlerPackage.packageName] : [])
|
|
2449
|
+
);
|
|
2450
|
+
return acceptedFamilies.size > 1 ? family : void 0;
|
|
2451
|
+
}
|
|
2452
|
+
function hintString(hint) {
|
|
2453
|
+
const fields = [
|
|
2454
|
+
["service", hint.servicePath],
|
|
2455
|
+
["operation", hint.operationPath],
|
|
2456
|
+
["package", hint.packageName],
|
|
2457
|
+
["repository", hint.repositoryName],
|
|
2458
|
+
["family", hint.candidateFamily],
|
|
2459
|
+
["repo", hint.implementationRepo]
|
|
2460
|
+
];
|
|
2461
|
+
return fields.flatMap(([key, value]) => value ? [`${key}=${value}`] : []).join(",");
|
|
2462
|
+
}
|
|
2463
|
+
function hintMatchesEdge(hint, evidence) {
|
|
2464
|
+
const model = evidence.modelPackage ?? evidence.candidates?.[0]?.modelPackage;
|
|
2465
|
+
const familyNames = /* @__PURE__ */ new Set([
|
|
2466
|
+
...(evidence.candidateFamilies ?? []).flatMap((family) => family.packageName ? [family.packageName] : []),
|
|
2467
|
+
...(evidence.candidates ?? []).flatMap((candidate) => candidate.handlerPackage?.packageName ? [candidate.handlerPackage.packageName] : [])
|
|
2468
|
+
]);
|
|
2469
|
+
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));
|
|
2470
|
+
}
|
|
2471
|
+
function candidateMatchesRepo(candidate, value) {
|
|
2472
|
+
return candidate.handlerPackage?.name === value || candidate.handlerPackage?.packageName === value || candidate.sourceFile?.startsWith(value) === true;
|
|
2473
|
+
}
|
|
2474
|
+
function matches(expected, actual) {
|
|
2475
|
+
return expected === void 0 || expected === actual;
|
|
2476
|
+
}
|
|
2477
|
+
function legacyHint(implementationRepo) {
|
|
2478
|
+
return { implementationRepo };
|
|
2479
|
+
}
|
|
2480
|
+
function asEvidence(value) {
|
|
2481
|
+
return value;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2121
2484
|
// src/linker/remote-query-target.ts
|
|
2122
2485
|
function buildRemoteQueryTarget(input) {
|
|
2123
2486
|
const entity = typeof input.queryEntity === "string" && input.queryEntity.trim() ? input.queryEntity.trim() : void 0;
|
|
@@ -2364,9 +2727,9 @@ function generatedFromConstantName(value) {
|
|
|
2364
2727
|
return void 0;
|
|
2365
2728
|
}
|
|
2366
2729
|
function resolved(value, raw) {
|
|
2367
|
-
const
|
|
2368
|
-
const generated = generatedFromConstantName(
|
|
2369
|
-
return { status: "resolved", operationName: generated ?? normalizedOperationName(
|
|
2730
|
+
const literal2 = clean(value);
|
|
2731
|
+
const generated = generatedFromConstantName(literal2);
|
|
2732
|
+
return { status: "resolved", operationName: generated ?? normalizedOperationName(literal2), raw };
|
|
2370
2733
|
}
|
|
2371
2734
|
function normalizeDecoratorOperationSignal(value, raw, candidateOperation) {
|
|
2372
2735
|
if (value) return resolved(value, raw);
|
|
@@ -2512,7 +2875,25 @@ function objectJson(value) {
|
|
|
2512
2875
|
}
|
|
2513
2876
|
function callEvidence(call, resolution, servicePath, op, destination, normalized, odataPathIntent) {
|
|
2514
2877
|
const bindingHasDynamicExpression = Boolean(Number(call.isDynamic ?? 0));
|
|
2515
|
-
|
|
2878
|
+
const routingPlaceholderKeys = placeholderKeys([servicePath, destination, stringValue2(call.aliasExpr), stringValue2(call.alias)]);
|
|
2879
|
+
return { sourceFile: call.source_file, sourceLine: call.source_line, file: call.source_file, line: call.source_line, callId: call.id, repo: call.repoName, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination, servicePath, operationPath: op, rawOperationPath: normalized?.wasInvocation ? normalized.rawOperationPath : odataPathIntent?.rawPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : void 0, invocationArguments: normalized?.wasInvocation ? normalized.invocationArguments : void 0, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : void 0, routingPlaceholderKeys: routingPlaceholderKeys.length ? routingPlaceholderKeys : void 0, odataOperationNormalizationReason: normalized?.normalizationReason, odataOperationNormalizationRejectedReason: normalized?.normalizationRejectedReason, localServiceName: call.local_service_name, localServiceLookup: call.local_service_lookup, aliasChain: parseJson(call.alias_chain_json), transport: call.call_type === "local_service_call" ? "local" : void 0, targetRepo: resolution.target?.repoName, targetServicePath: resolution.target?.servicePath, targetOperationPath: resolution.target?.operationPath, targetOperation: resolution.target?.operationName, helperChain: parseJson(call.helperChainJson), candidates: resolution.candidates, candidateScores: compactCandidateScores(resolution.candidates), candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, odataPathIntent, queryStringPresent: odataPathIntent?.hasQueryString || void 0, queryPlaceholderKeys: odataPathIntent?.placeholderKeys.length ? odataPathIntent.placeholderKeys : void 0, bindingHasDynamicExpression: bindingHasDynamicExpression || void 0, outboundEvidence: objectJson(call.evidence_json), analysisCompleteness: call.unresolved_reason ? "partial" : "complete", parserWarning: call.unresolved_reason ? { code: "parser_warning", message: call.unresolved_reason } : void 0 };
|
|
2880
|
+
}
|
|
2881
|
+
function compactCandidateScores(candidates) {
|
|
2882
|
+
return candidates.flatMap((candidate) => {
|
|
2883
|
+
const row = objectValue(candidate);
|
|
2884
|
+
if (!row) return [];
|
|
2885
|
+
return [{ repo: row.repoName, servicePath: row.servicePath, operationPath: row.operationPath, score: row.score, reasons: row.reasons }];
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
function placeholderKeys(values) {
|
|
2889
|
+
const keys = values.flatMap((value) => [...(value ?? "").matchAll(/\$\{([^}]*)\}/g)].map((match) => (match[1] ?? "").trim()).filter(Boolean));
|
|
2890
|
+
return [...new Set(keys)].sort();
|
|
2891
|
+
}
|
|
2892
|
+
function objectValue(value) {
|
|
2893
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2894
|
+
}
|
|
2895
|
+
function stringValue2(value) {
|
|
2896
|
+
return typeof value === "string" ? value : void 0;
|
|
2516
2897
|
}
|
|
2517
2898
|
function linkImplementations(db, workspaceId, generation) {
|
|
2518
2899
|
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);
|
|
@@ -2544,13 +2925,14 @@ function linkImplementations(db, workspaceId, generation) {
|
|
|
2544
2925
|
candidateFamilies: duplicateFamilies,
|
|
2545
2926
|
candidates: candidates.map((candidate, index) => candidateEvidence(candidate, index + 1))
|
|
2546
2927
|
};
|
|
2928
|
+
const evidenceWithHints = unique ? evidence : { ...evidence, implementationHintSuggestions: implementationHintSuggestions(evidence) };
|
|
2547
2929
|
if (accepted.length === 0) {
|
|
2548
|
-
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "OPERATION_IMPLEMENTED_BY_HANDLER", "unresolved", "operation", graphId(operation.operationId), "handler_method_candidates", candidates.map((row) => graphId(row.methodId)).join(","), 0, JSON.stringify(
|
|
2930
|
+
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "OPERATION_IMPLEMENTED_BY_HANDLER", "unresolved", "operation", graphId(operation.operationId), "handler_method_candidates", candidates.map((row) => graphId(row.methodId)).join(","), 0, JSON.stringify(evidenceWithHints), 0, "No implementation candidate passed policy", generation);
|
|
2549
2931
|
edgeCount += 1;
|
|
2550
2932
|
unresolvedCount += 1;
|
|
2551
2933
|
continue;
|
|
2552
2934
|
}
|
|
2553
|
-
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(
|
|
2935
|
+
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "OPERATION_IMPLEMENTED_BY_HANDLER", 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(evidenceWithHints), 0, unique ? null : "Ambiguous registered handler implementation candidates", generation);
|
|
2554
2936
|
edgeCount += 1;
|
|
2555
2937
|
if (unique) resolvedCount += 1;
|
|
2556
2938
|
else ambiguousCount += 1;
|
|
@@ -2841,20 +3223,20 @@ function runtimeVariableDiagnostic(edges) {
|
|
|
2841
3223
|
}
|
|
2842
3224
|
function edgeTarget(row, evidence) {
|
|
2843
3225
|
const effective = parseObject(evidence.effectiveResolution);
|
|
2844
|
-
const targetServicePath =
|
|
2845
|
-
const targetOperationPath =
|
|
3226
|
+
const targetServicePath = stringValue3(effective.targetServicePath ?? evidence.targetServicePath);
|
|
3227
|
+
const targetOperationPath = stringValue3(effective.targetOperationPath ?? evidence.targetOperationPath);
|
|
2846
3228
|
if (targetServicePath && targetOperationPath) return `${targetServicePath}${targetOperationPath}`;
|
|
2847
3229
|
const runtimeCandidate = evidence.runtimeResolvedCandidate;
|
|
2848
3230
|
if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath) return `${runtimeCandidate.servicePath}${runtimeCandidate.operationPath}`;
|
|
2849
|
-
const servicePath =
|
|
2850
|
-
const operationPath =
|
|
2851
|
-
const targetOperation =
|
|
2852
|
-
const targetRepo =
|
|
3231
|
+
const servicePath = stringValue3(evidence.servicePath);
|
|
3232
|
+
const operationPath = stringValue3(evidence.operationPath);
|
|
3233
|
+
const targetOperation = stringValue3(evidence.targetOperation);
|
|
3234
|
+
const targetRepo = stringValue3(evidence.targetRepo) ?? "";
|
|
2853
3235
|
if (row.edge_type === "HANDLER_RUNS_DB_QUERY") return `Entity: ${row.to_id || "unknown"}`;
|
|
2854
|
-
if (row.edge_type === "HANDLER_RUNS_REMOTE_QUERY") return
|
|
3236
|
+
if (row.edge_type === "HANDLER_RUNS_REMOTE_QUERY") return stringValue3(evidence.remoteQueryTarget) ?? `Remote query: ${row.to_id || "unknown"}`;
|
|
2855
3237
|
if (row.edge_type === "HANDLER_CALLS_EXTERNAL_HTTP") {
|
|
2856
3238
|
const target = parseObject(evidence.externalTarget);
|
|
2857
|
-
return
|
|
3239
|
+
return stringValue3(target.label) ?? `External endpoint: ${row.to_id || "unknown"}`;
|
|
2858
3240
|
}
|
|
2859
3241
|
if (servicePath && operationPath) return `${servicePath}${operationPath}`;
|
|
2860
3242
|
return targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
|
|
@@ -2893,10 +3275,12 @@ function withEffectiveResolution(evidence, row, unresolvedReason, resolution) {
|
|
|
2893
3275
|
return { ...rest, effectiveResolution: current, linker: { status: current.status, confidence: current.confidence, reason: unresolvedReason, edgeType: current.edgeType } };
|
|
2894
3276
|
}
|
|
2895
3277
|
function resolveRuntimeOperation(db, evidence, workspaceId) {
|
|
2896
|
-
const servicePath =
|
|
2897
|
-
const
|
|
2898
|
-
const
|
|
2899
|
-
const
|
|
3278
|
+
const servicePath = stringValue3(evidence.servicePath);
|
|
3279
|
+
const rawOperationPath = stringValue3(evidence.operationPath);
|
|
3280
|
+
const normalized = normalizeODataOperationInvocationPath(rawOperationPath);
|
|
3281
|
+
const operationPath = normalized?.wasInvocation ? normalized.normalizedOperationPath : stringValue3(evidence.normalizedOperationPath) ?? rawOperationPath;
|
|
3282
|
+
const alias = stringValue3(evidence.serviceAliasExpr ?? evidence.serviceAlias);
|
|
3283
|
+
const destination = stringValue3(evidence.destination);
|
|
2900
3284
|
return resolveOperation(db, { servicePath, operationPath, alias, destination, hasExplicitOverride: true, isDynamic: true }, workspaceId);
|
|
2901
3285
|
}
|
|
2902
3286
|
function evidenceWithRuntimeVariables(evidence, vars) {
|
|
@@ -2911,7 +3295,7 @@ function evidenceWithRuntimeVariables(evidence, vars) {
|
|
|
2911
3295
|
function runtimeSubstitutions(evidence, vars) {
|
|
2912
3296
|
const substitutions = {};
|
|
2913
3297
|
for (const key of ["servicePath", "operationPath", "serviceAliasExpr", "serviceAlias", "destination"]) {
|
|
2914
|
-
const substitution = substituteVariables(
|
|
3298
|
+
const substitution = substituteVariables(stringValue3(evidence[key]), vars);
|
|
2915
3299
|
if (substitution.placeholders.length > 0) substitutions[key] = substitution;
|
|
2916
3300
|
}
|
|
2917
3301
|
return substitutions;
|
|
@@ -2933,7 +3317,7 @@ function runtimeUnresolvedReason(resolution) {
|
|
|
2933
3317
|
function parseObject(value) {
|
|
2934
3318
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2935
3319
|
}
|
|
2936
|
-
function
|
|
3320
|
+
function stringValue3(value) {
|
|
2937
3321
|
return typeof value === "string" ? value : void 0;
|
|
2938
3322
|
}
|
|
2939
3323
|
|
|
@@ -2945,7 +3329,7 @@ function normalizeOperation(value) {
|
|
|
2945
3329
|
function positiveDepth(value) {
|
|
2946
3330
|
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 25;
|
|
2947
3331
|
}
|
|
2948
|
-
function operationStartScope(db, repoId, start,
|
|
3332
|
+
function operationStartScope(db, repoId, start, hintOptions) {
|
|
2949
3333
|
const requested = normalizeOperation(start.operationPath ?? start.operation);
|
|
2950
3334
|
if (!requested) return void 0;
|
|
2951
3335
|
const rows2 = db.prepare(`SELECT o.id operationId,o.operation_name operationName,o.operation_path operationPath,s.service_path servicePath,r.id repoId,r.name repoName
|
|
@@ -2961,14 +3345,16 @@ function operationStartScope(db, repoId, start, implementationRepo) {
|
|
|
2961
3345
|
const operationId = String(rows2[0]?.operationId);
|
|
2962
3346
|
const impl = implementationScope(db, operationId);
|
|
2963
3347
|
if (impl.edge?.status === "resolved" && impl.files.size > 0) return { files: impl.files, symbols: impl.symbolId ? /* @__PURE__ */ new Set([impl.symbolId]) : void 0, operationId, diagnostics: [] };
|
|
2964
|
-
const hinted = implementationMethodIdFromHint(impl.edge,
|
|
3348
|
+
const hinted = implementationMethodIdFromHint(impl.edge, hintOptions);
|
|
2965
3349
|
if (hinted.methodId) {
|
|
2966
3350
|
const hintedScope = handlerScope(db, hinted.methodId);
|
|
2967
3351
|
if (hintedScope?.files.size) return { files: hintedScope.files, symbols: hintedScope.symbolId ? /* @__PURE__ */ new Set([hintedScope.symbolId]) : void 0, operationId, diagnostics: [] };
|
|
2968
3352
|
}
|
|
2969
3353
|
if (impl.edge) {
|
|
2970
3354
|
const evidence = parseEvidence(impl.edge.evidence_json);
|
|
2971
|
-
|
|
3355
|
+
const hintDiagnostic = implementationHintDiagnostic(hinted, evidence.implementationHintSuggestions);
|
|
3356
|
+
const diagnostics = [{ severity: "warning", code: impl.edge.status === "ambiguous" ? "trace_start_ambiguous" : "trace_start_implementation_unresolved", message: `Indexed operation matched but implementation edge is ${String(impl.edge.status ?? "unresolved")}`, resolutionStage: "implementation", resolutionStatus: impl.edge.status === "ambiguous" ? "ambiguous_implementation" : "rejected_implementation", implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, implementationAmbiguityReasons: evidence.ambiguityReasons, implementationHintSuggestions: evidence.implementationHintSuggestions, candidates: evidence.candidates }];
|
|
3357
|
+
return { operationId, diagnostics: hintDiagnostic ? [hintDiagnostic, ...diagnostics] : diagnostics };
|
|
2972
3358
|
}
|
|
2973
3359
|
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" }] };
|
|
2974
3360
|
}
|
|
@@ -3012,12 +3398,12 @@ function sourceFilesForStart(db, repoId, start) {
|
|
|
3012
3398
|
}
|
|
3013
3399
|
return void 0;
|
|
3014
3400
|
}
|
|
3015
|
-
function startScope(db, start,
|
|
3401
|
+
function startScope(db, start, hintOptions) {
|
|
3016
3402
|
const repo = start.repo ? db.prepare(
|
|
3017
3403
|
"SELECT id,name FROM repositories WHERE name=? OR package_name=?"
|
|
3018
3404
|
).get(start.repo, start.repo) : void 0;
|
|
3019
3405
|
if (start.repo && !repo) return { repo, selectorMatched: false };
|
|
3020
|
-
const operationScope = operationStartScope(db, repo?.id, start,
|
|
3406
|
+
const operationScope = operationStartScope(db, repo?.id, start, hintOptions);
|
|
3021
3407
|
const terminalOperationScope = operationScope && !operationScope.files && (operationScope.diagnostics ?? []).some((d) => d.resolutionStage === "operation" || d.resolutionStage === "implementation");
|
|
3022
3408
|
const sourceScope = operationScope?.files || terminalOperationScope ? operationScope : sourceFilesForStart(db, repo?.id, start);
|
|
3023
3409
|
const sourceFiles = sourceScope?.files;
|
|
@@ -3064,27 +3450,15 @@ function implementationScope(db, operationId) {
|
|
|
3064
3450
|
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);
|
|
3065
3451
|
return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
|
|
3066
3452
|
}
|
|
3067
|
-
function implementationMethodIdFromHint(edge,
|
|
3068
|
-
if (!edge || edge.status !== "ambiguous"
|
|
3069
|
-
|
|
3070
|
-
const matches = (evidence.candidates ?? []).filter((item) => item.accepted && (item.handlerPackage?.name === implementationRepo || item.handlerPackage?.packageName === implementationRepo || item.sourceFile?.startsWith(implementationRepo)));
|
|
3071
|
-
if (matches.length !== 1 || matches[0]?.methodId === void 0) return { evidence: { status: matches.length > 1 ? "tied" : "not_matched", strategy: "implementation_repo_hint", selectedRepo: implementationRepo, candidateCount: matches.length } };
|
|
3072
|
-
return {
|
|
3073
|
-
methodId: String(matches[0].methodId),
|
|
3074
|
-
evidence: {
|
|
3075
|
-
status: "selected",
|
|
3076
|
-
guided: true,
|
|
3077
|
-
strategy: "implementation_repo_hint",
|
|
3078
|
-
selectedRepo: implementationRepo,
|
|
3079
|
-
selectedMethodId: matches[0].methodId,
|
|
3080
|
-
ambiguityReason: evidence.ambiguityReasons?.[0]
|
|
3081
|
-
}
|
|
3082
|
-
};
|
|
3453
|
+
function implementationMethodIdFromHint(edge, options) {
|
|
3454
|
+
if (!edge || edge.status !== "ambiguous") return { blocksAutomatic: false, evidence: { status: "not_applicable" } };
|
|
3455
|
+
return selectImplementation(parseEvidence(edge.evidence_json), options.implementationHints, options.implementationRepo);
|
|
3083
3456
|
}
|
|
3084
|
-
function contextImplementationMethodId(edge, callerRepoId, remoteEvidence
|
|
3085
|
-
const hinted = implementationMethodIdFromHint(edge,
|
|
3457
|
+
function contextImplementationMethodId(edge, callerRepoId, remoteEvidence, hintOptions) {
|
|
3458
|
+
const hinted = implementationMethodIdFromHint(edge, hintOptions);
|
|
3086
3459
|
if (hinted.methodId) return hinted;
|
|
3087
|
-
if (
|
|
3460
|
+
if (hinted.blocksAutomatic) return hinted;
|
|
3461
|
+
if (!edge || edge.status !== "ambiguous" || callerRepoId === void 0) return hinted;
|
|
3088
3462
|
const evidence = JSON.parse(String(edge.evidence_json || "{}"));
|
|
3089
3463
|
const scores = (evidence.candidates ?? []).filter((item) => item.accepted).map((item) => {
|
|
3090
3464
|
const reasons = [];
|
|
@@ -3103,10 +3477,10 @@ function contextImplementationMethodId(edge, callerRepoId, remoteEvidence = {},
|
|
|
3103
3477
|
}
|
|
3104
3478
|
return { methodId: item.methodId, score, reasons, handlerPackage: item.handlerPackage, applicationPackage: item.applicationPackage };
|
|
3105
3479
|
}).sort((a, b) => b.score - a.score);
|
|
3106
|
-
if (scores.length === 0) return { evidence: { status: "not_applicable", candidateScores: [] } };
|
|
3480
|
+
if (scores.length === 0) return { blocksAutomatic: false, evidence: { status: "not_applicable", candidateScores: [] } };
|
|
3107
3481
|
const [first, second] = scores;
|
|
3108
|
-
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 } };
|
|
3109
|
-
return { evidence: { status: "tied", tieReason: scores.length > 1 ? "duplicate_helper_implementation_candidates" : "no_unique_materially_stronger_candidate", candidateScores: scores } };
|
|
3482
|
+
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 } };
|
|
3483
|
+
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 } };
|
|
3110
3484
|
}
|
|
3111
3485
|
function handlerScope(db, methodId) {
|
|
3112
3486
|
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);
|
|
@@ -3204,18 +3578,22 @@ function contextForSymbolCall(db, symbolCall, callerBindings) {
|
|
|
3204
3578
|
const next = /* @__PURE__ */ new Map();
|
|
3205
3579
|
if (callerBindings.size === 0) return next;
|
|
3206
3580
|
const callEvidence2 = parseEvidence(symbolCall.evidence_json);
|
|
3207
|
-
const callee = db.prepare("SELECT evidence_json evidenceJson FROM symbols WHERE id=?").get(symbolCall.callee_symbol_id);
|
|
3581
|
+
const callee = db.prepare("SELECT evidence_json evidenceJson,source_file sourceFile,start_line startLine FROM symbols WHERE id=?").get(symbolCall.callee_symbol_id);
|
|
3208
3582
|
const calleeEvidence = parseEvidence(callee?.evidenceJson);
|
|
3209
3583
|
const params = Array.isArray(calleeEvidence.parameters) ? calleeEvidence.parameters.filter((item) => typeof item === "string") : [];
|
|
3210
3584
|
const parameterBindings = Array.isArray(calleeEvidence.parameterBindings) ? calleeEvidence.parameterBindings.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
|
|
3211
3585
|
const parameterPropertyAliases = Array.isArray(calleeEvidence.parameterPropertyAliases) ? calleeEvidence.parameterPropertyAliases.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
|
|
3212
3586
|
const args = Array.isArray(callEvidence2.callArguments) ? callEvidence2.callArguments : [];
|
|
3587
|
+
const provenance = {
|
|
3588
|
+
callerSite: { sourceFile: String(symbolCall.source_file ?? ""), sourceLine: Number(symbolCall.source_line ?? 0) },
|
|
3589
|
+
calleeSite: { sourceFile: callee?.sourceFile, sourceLine: callee?.startLine }
|
|
3590
|
+
};
|
|
3213
3591
|
args.forEach((arg, index) => {
|
|
3214
3592
|
const paramBinding = parameterBindings.find((binding) => binding.index === index);
|
|
3215
3593
|
const param = paramBinding?.kind === "identifier" && typeof paramBinding.name === "string" ? paramBinding.name : params[index];
|
|
3216
3594
|
if (arg.kind === "identifier" && typeof arg.name === "string") {
|
|
3217
3595
|
const binding = callerBindings.get(arg.name);
|
|
3218
|
-
if (binding && param) next.set(param, { ...binding, source: "local_symbol_argument", callerArgument: arg.name, calleeParameter: param, calleeReceiver: param });
|
|
3596
|
+
if (binding && param) next.set(param, { ...binding, ...provenance, source: "local_symbol_argument", callerArgument: arg.name, calleeParameter: param, calleeReceiver: param });
|
|
3219
3597
|
}
|
|
3220
3598
|
if (arg.kind === "object_literal" && Array.isArray(arg.properties)) {
|
|
3221
3599
|
for (const prop of arg.properties) {
|
|
@@ -3223,15 +3601,23 @@ function contextForSymbolCall(db, symbolCall, callerBindings) {
|
|
|
3223
3601
|
const binding = callerBindings.get(prop.argument);
|
|
3224
3602
|
if (!binding) continue;
|
|
3225
3603
|
const destructured = paramBinding?.kind === "object_pattern" && Array.isArray(paramBinding.properties) ? paramBinding.properties.find((item) => item.property === prop.property && typeof item.local === "string") : void 0;
|
|
3226
|
-
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 });
|
|
3604
|
+
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 });
|
|
3227
3605
|
else if (param) {
|
|
3228
|
-
next.set(`${param}.${prop.property}`, { ...binding, source: "local_symbol_object_argument", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeReceiver: `${param}.${prop.property}` });
|
|
3606
|
+
next.set(`${param}.${prop.property}`, { ...binding, ...provenance, source: "local_symbol_object_argument", callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeReceiver: `${param}.${prop.property}` });
|
|
3229
3607
|
for (const alias of parameterPropertyAliases) {
|
|
3230
|
-
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 });
|
|
3608
|
+
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 });
|
|
3231
3609
|
}
|
|
3232
3610
|
}
|
|
3233
3611
|
}
|
|
3234
3612
|
}
|
|
3613
|
+
if (arg.kind === "array_literal" && Array.isArray(arg.elements) && paramBinding?.kind === "array_pattern" && Array.isArray(paramBinding.elements)) {
|
|
3614
|
+
for (const element of arg.elements) {
|
|
3615
|
+
const target = paramBinding.elements.find((item) => item.index === element.index);
|
|
3616
|
+
if (element.kind !== "identifier" || typeof element.name !== "string" || typeof target?.local !== "string") continue;
|
|
3617
|
+
const binding = callerBindings.get(element.name);
|
|
3618
|
+
if (binding) next.set(target.local, { ...binding, ...provenance, source: "local_symbol_destructured_array_argument", callerArgument: element.name, calleeParameter: String(index), calleeReceiver: target.local });
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3235
3621
|
});
|
|
3236
3622
|
return next;
|
|
3237
3623
|
}
|
|
@@ -3242,7 +3628,7 @@ function contextualRuntimeResolution(db, call, binding, workspaceId, persistedRo
|
|
|
3242
3628
|
const servicePath = binding.effectiveServicePath ?? binding.servicePathExpr ?? binding.requireServicePath;
|
|
3243
3629
|
const destination = binding.effectiveDestination ?? binding.destinationExpr ?? binding.requireDestination;
|
|
3244
3630
|
const resolution = resolveOperation(db, { servicePath, operationPath: op, alias: binding.aliasExpr ?? binding.alias, destination, hasExplicitOverride: true, isDynamic: false }, workspaceId);
|
|
3245
|
-
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 };
|
|
3631
|
+
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 };
|
|
3246
3632
|
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" };
|
|
3247
3633
|
const resolvedEvidence = { ...evidence, contextualServiceBindingSelected: true, targetRepo: resolution.target.repoName, targetServicePath: resolution.target.servicePath, targetOperationPath: resolution.target.operationPath, targetOperation: resolution.target.operationName };
|
|
3248
3634
|
const persistedResolved = persistedRows.find((item) => item.status === "resolved");
|
|
@@ -3250,7 +3636,8 @@ function contextualRuntimeResolution(db, call, binding, workspaceId, persistedRo
|
|
|
3250
3636
|
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 };
|
|
3251
3637
|
}
|
|
3252
3638
|
function trace(db, start, options) {
|
|
3253
|
-
const
|
|
3639
|
+
const hintOptions = { implementationRepo: options.implementationRepo, implementationHints: options.implementationHints };
|
|
3640
|
+
const scope = startScope(db, start, hintOptions);
|
|
3254
3641
|
const diagnostics = db.prepare(
|
|
3255
3642
|
"SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics WHERE (? IS NULL OR repo_id=?)"
|
|
3256
3643
|
).all(scope.repo?.id, scope.repo?.id);
|
|
@@ -3273,7 +3660,7 @@ function trace(db, start, options) {
|
|
|
3273
3660
|
const op = operationNode(db, scope.startOperationId);
|
|
3274
3661
|
const impl = implementationScope(db, scope.startOperationId);
|
|
3275
3662
|
if (op) nodes.set(String(op.id), op);
|
|
3276
|
-
const startSelection = implementationMethodIdFromHint(impl.edge,
|
|
3663
|
+
const startSelection = implementationMethodIdFromHint(impl.edge, hintOptions);
|
|
3277
3664
|
if (impl.edge && (impl.edge.status === "resolved" || startSelection.methodId)) {
|
|
3278
3665
|
const selectedMethodId = impl.edge.status === "resolved" ? impl.edge.to_id : startSelection.methodId;
|
|
3279
3666
|
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 };
|
|
@@ -3360,11 +3747,13 @@ function trace(db, start, options) {
|
|
|
3360
3747
|
});
|
|
3361
3748
|
if (effectiveRow.to_kind === "operation") {
|
|
3362
3749
|
const implementation = implementationScope(db, effectiveRow.to_id);
|
|
3363
|
-
const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence,
|
|
3750
|
+
const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence, hintOptions);
|
|
3364
3751
|
const contextMethodId = contextSelection.methodId;
|
|
3365
3752
|
const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : void 0;
|
|
3366
3753
|
if (implementation.edge) {
|
|
3367
3754
|
const implEvidence = JSON.parse(String(implementation.edge.evidence_json || "{}"));
|
|
3755
|
+
const hintDiagnostic = implementationHintDiagnostic(contextSelection, implEvidence.implementationHintSuggestions);
|
|
3756
|
+
if (hintDiagnostic) diagnostics.unshift(hintDiagnostic);
|
|
3368
3757
|
const handlerNode = implementation.edge.status === "resolved" ? handlerMethodNode(db, implementation.edge.to_id) : contextNode;
|
|
3369
3758
|
const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
|
|
3370
3759
|
if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
|
|
@@ -3373,7 +3762,7 @@ function trace(db, start, options) {
|
|
|
3373
3762
|
type: "operation_implemented_by_handler",
|
|
3374
3763
|
from: to,
|
|
3375
3764
|
to: implTo,
|
|
3376
|
-
evidence: contextMethodId ? { ...implEvidence, contextualImplementationSelected: contextSelection.evidence.strategy !== "implementation_repo_hint", contextualImplementation: contextSelection.evidence, implementationSelection: contextSelection.evidence } : { ...implEvidence, contextualImplementation: contextSelection.evidence },
|
|
3765
|
+
evidence: contextMethodId ? { ...implEvidence, contextualImplementationSelected: contextSelection.evidence.strategy !== "implementation_repo_hint", contextualImplementation: contextSelection.evidence, implementationSelection: contextSelection.evidence } : { ...implEvidence, contextualImplementation: contextSelection.evidence, implementationSelection: contextSelection.evidence },
|
|
3377
3766
|
confidence: Number(implementation.edge.confidence ?? 0),
|
|
3378
3767
|
unresolvedReason: implementation.edge.status === "resolved" || contextMethodId ? void 0 : String(implementation.edge.unresolved_reason ?? implementation.edge.status)
|
|
3379
3768
|
});
|
|
@@ -3426,13 +3815,15 @@ export {
|
|
|
3426
3815
|
redactValue,
|
|
3427
3816
|
normalizeODataOperationInvocationPath,
|
|
3428
3817
|
classifyODataPathIntent,
|
|
3818
|
+
parseServiceBindings,
|
|
3429
3819
|
containsSupportedOutboundCall,
|
|
3430
3820
|
parseOutboundCalls,
|
|
3431
|
-
parseServiceBindings,
|
|
3432
3821
|
applyVariables,
|
|
3433
3822
|
extractPlaceholders,
|
|
3434
3823
|
substituteVariables,
|
|
3824
|
+
parseImplementationHint,
|
|
3825
|
+
implementationHintSuggestions,
|
|
3435
3826
|
linkWorkspace,
|
|
3436
3827
|
trace
|
|
3437
3828
|
};
|
|
3438
|
-
//# sourceMappingURL=chunk-
|
|
3829
|
+
//# sourceMappingURL=chunk-EGY2A4AT.js.map
|