@saptools/service-flow 0.1.48 → 0.1.49
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 +7 -0
- package/README.md +10 -0
- package/dist/{chunk-EGY2A4AT.js → chunk-XOROZHW4.js} +593 -242
- package/dist/chunk-XOROZHW4.js.map +1 -0
- package/dist/cli.js +265 -24
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/doctor.ts +128 -2
- package/src/db/repositories.ts +186 -10
- package/src/linker/cross-repo-linker.ts +70 -1
- package/src/output/table-output.ts +18 -8
- package/src/parsers/imported-wrapper-parser.ts +20 -46
- package/src/parsers/operation-path-analysis.ts +350 -0
- package/src/parsers/outbound-call-parser.ts +63 -53
- package/src/trace/evidence.ts +18 -3
- package/src/trace/trace-engine.ts +118 -25
- package/dist/chunk-EGY2A4AT.js.map +0 -1
|
@@ -1276,7 +1276,7 @@ function collectClassHelpers(sf) {
|
|
|
1276
1276
|
// src/parsers/outbound-call-parser.ts
|
|
1277
1277
|
import fs7 from "fs/promises";
|
|
1278
1278
|
import path10 from "path";
|
|
1279
|
-
import
|
|
1279
|
+
import ts8 from "typescript";
|
|
1280
1280
|
|
|
1281
1281
|
// src/linker/external-http-target.ts
|
|
1282
1282
|
import { createHash } from "crypto";
|
|
@@ -1552,7 +1552,242 @@ function topLevelQueryIndex(text) {
|
|
|
1552
1552
|
|
|
1553
1553
|
// src/parsers/imported-wrapper-parser.ts
|
|
1554
1554
|
import path9 from "path";
|
|
1555
|
+
import ts7 from "typescript";
|
|
1556
|
+
|
|
1557
|
+
// src/parsers/operation-path-analysis.ts
|
|
1555
1558
|
import ts6 from "typescript";
|
|
1559
|
+
var maxAliasDepth = 6;
|
|
1560
|
+
function analyzeOperationPath(expression, use, method = "POST") {
|
|
1561
|
+
if (!expression) return emptyAnalysis();
|
|
1562
|
+
const state = collectExpressionState(expression, use, 0, /* @__PURE__ */ new Set());
|
|
1563
|
+
const paths = unique(state.paths.map(normalizeRawPath));
|
|
1564
|
+
const normalized = unique(paths.flatMap((value) => normalizedCandidate(value, method)));
|
|
1565
|
+
const status = pathStatus(paths, state.placeholders, state.dynamic);
|
|
1566
|
+
const runtimeIdentifier = state.dynamic.at(-1)?.expression;
|
|
1567
|
+
return {
|
|
1568
|
+
status,
|
|
1569
|
+
rawExpression: expression.getText(expression.getSourceFile()),
|
|
1570
|
+
normalizedOperationPath: status === "static" && normalized.length === 1 ? normalized[0] : void 0,
|
|
1571
|
+
candidateRawPaths: paths,
|
|
1572
|
+
candidateNormalizedOperationPaths: normalized,
|
|
1573
|
+
placeholderKeys: unique([...state.placeholders, ...runtimeIdentifier ? [runtimeIdentifier] : []]),
|
|
1574
|
+
sourceKind: unique(state.sourceKinds).join("+") || "unknown",
|
|
1575
|
+
candidateIdentifier: ts6.isIdentifier(expression) ? expression.text : void 0,
|
|
1576
|
+
runtimeIdentifier,
|
|
1577
|
+
dynamicReassignments: state.dynamic,
|
|
1578
|
+
lexicalScope: lexicalEvidence(expression, use)
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
function operationPathExpression(analysis) {
|
|
1582
|
+
if (analysis.status === "ambiguous" || analysis.status === "unknown") return void 0;
|
|
1583
|
+
if (analysis.sourceKind.includes("parameter_binding")) return void 0;
|
|
1584
|
+
if (analysis.candidateRawPaths.length === 1 && analysis.dynamicReassignments.length === 0)
|
|
1585
|
+
return analysis.candidateRawPaths[0];
|
|
1586
|
+
if (!analysis.runtimeIdentifier || analysis.sourceKind.includes("binding_not_found"))
|
|
1587
|
+
return void 0;
|
|
1588
|
+
return isRuntimeIdentifier(analysis.runtimeIdentifier) ? `\${${analysis.runtimeIdentifier}}` : void 0;
|
|
1589
|
+
}
|
|
1590
|
+
function pathUnresolvedReason(analysis) {
|
|
1591
|
+
if (analysis.status === "ambiguous") return "ambiguous_operation_path_candidates";
|
|
1592
|
+
if (analysis.status === "dynamic" && !operationPathExpression(analysis))
|
|
1593
|
+
return "dynamic_operation_path_identifier";
|
|
1594
|
+
if (analysis.dynamicReassignments.length > 0) return "dynamic_operation_path_identifier";
|
|
1595
|
+
return void 0;
|
|
1596
|
+
}
|
|
1597
|
+
function collectExpressionState(expression, use, depth, seen) {
|
|
1598
|
+
const unwrapped = unwrap(expression);
|
|
1599
|
+
if (ts6.isStringLiteral(unwrapped)) return staticState(unwrapped.text, "string_literal");
|
|
1600
|
+
if (ts6.isNoSubstitutionTemplateLiteral(unwrapped))
|
|
1601
|
+
return staticState(unwrapped.text, "no_substitution_template");
|
|
1602
|
+
if (ts6.isTemplateExpression(unwrapped)) return templateState(unwrapped);
|
|
1603
|
+
if (ts6.isConditionalExpression(unwrapped))
|
|
1604
|
+
return mergeStates([
|
|
1605
|
+
collectExpressionState(unwrapped.whenTrue, use, depth + 1, seen),
|
|
1606
|
+
collectExpressionState(unwrapped.whenFalse, use, depth + 1, seen)
|
|
1607
|
+
], "conditional_candidates");
|
|
1608
|
+
if (ts6.isIdentifier(unwrapped))
|
|
1609
|
+
return collectIdentifierState(unwrapped, use, depth, seen);
|
|
1610
|
+
return dynamicState(unwrapped);
|
|
1611
|
+
}
|
|
1612
|
+
function collectIdentifierState(identifier, use, depth, seen) {
|
|
1613
|
+
if (depth >= maxAliasDepth)
|
|
1614
|
+
return dynamicState(identifier, "alias_depth_exceeded");
|
|
1615
|
+
const binding = resolveBinding(identifier, use);
|
|
1616
|
+
if (!binding || seen.has(binding.declaration))
|
|
1617
|
+
return dynamicState(identifier, binding ? "alias_cycle" : "binding_not_found");
|
|
1618
|
+
seen.add(binding.declaration);
|
|
1619
|
+
if (ts6.isParameter(binding.declaration))
|
|
1620
|
+
return dynamicState(identifier, "parameter_binding");
|
|
1621
|
+
const expressions = reachingExpressions(binding.declaration, use);
|
|
1622
|
+
if (expressions.length === 0) return dynamicState(identifier, "initializer_missing");
|
|
1623
|
+
const states = expressions.map((item) => collectExpressionState(item.expression, item.node, depth + 1, seen));
|
|
1624
|
+
const merged = mergeStates(states, binding.immutable ? "const_alias" : "mutable_alias");
|
|
1625
|
+
if (merged.dynamic.length === 0) return merged;
|
|
1626
|
+
return {
|
|
1627
|
+
...merged,
|
|
1628
|
+
dynamic: merged.dynamic.map((item) => item.expression === identifier.text ? item : item)
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
function reachingExpressions(declaration, use) {
|
|
1632
|
+
const rows2 = [];
|
|
1633
|
+
if (declaration.initializer) rows2.push({ expression: declaration.initializer, node: declaration });
|
|
1634
|
+
if ((declaration.parent.flags & ts6.NodeFlags.Const) !== 0) return rows2;
|
|
1635
|
+
const source = use.getSourceFile();
|
|
1636
|
+
const visit = (node) => {
|
|
1637
|
+
if (node.getStart(source) >= use.getStart(source)) return;
|
|
1638
|
+
if (node !== source && ts6.isFunctionLike(node) && !contains(node, use)) return;
|
|
1639
|
+
if (isAssignmentTo(node, declaration, use))
|
|
1640
|
+
rows2.push({ expression: node.right, node });
|
|
1641
|
+
ts6.forEachChild(node, visit);
|
|
1642
|
+
};
|
|
1643
|
+
visit(source);
|
|
1644
|
+
return rows2.sort((left, right) => left.node.getStart(source) - right.node.getStart(source));
|
|
1645
|
+
}
|
|
1646
|
+
function isAssignmentTo(node, declaration, use) {
|
|
1647
|
+
if (!ts6.isBinaryExpression(node) || node.operatorToken.kind !== ts6.SyntaxKind.EqualsToken)
|
|
1648
|
+
return false;
|
|
1649
|
+
if (!ts6.isIdentifier(node.left) || !ts6.isIdentifier(declaration.name)) return false;
|
|
1650
|
+
return resolveBinding(node.left, node)?.declaration === declaration && contains(declarationScope(declaration), use);
|
|
1651
|
+
}
|
|
1652
|
+
function resolveBinding(identifier, use) {
|
|
1653
|
+
const source = use.getSourceFile();
|
|
1654
|
+
const matches2 = [];
|
|
1655
|
+
const visit = (node) => {
|
|
1656
|
+
if (isNamedDeclaration(node, identifier.text) && isAccessible(node, use))
|
|
1657
|
+
matches2.push(node);
|
|
1658
|
+
ts6.forEachChild(node, visit);
|
|
1659
|
+
};
|
|
1660
|
+
visit(source);
|
|
1661
|
+
const declaration = matches2.sort((left, right) => right.getStart(source) - left.getStart(source))[0];
|
|
1662
|
+
if (!declaration) return void 0;
|
|
1663
|
+
const immutable = ts6.isVariableDeclaration(declaration) && (declaration.parent.flags & ts6.NodeFlags.Const) !== 0;
|
|
1664
|
+
return { declaration, immutable };
|
|
1665
|
+
}
|
|
1666
|
+
function isNamedDeclaration(node, name) {
|
|
1667
|
+
return (ts6.isVariableDeclaration(node) || ts6.isParameter(node)) && ts6.isIdentifier(node.name) && node.name.text === name;
|
|
1668
|
+
}
|
|
1669
|
+
function isAccessible(declaration, use) {
|
|
1670
|
+
const source = use.getSourceFile();
|
|
1671
|
+
if (declaration.name.getStart(source) >= use.getStart(source)) return false;
|
|
1672
|
+
const scope = declarationScope(declaration);
|
|
1673
|
+
return ts6.isSourceFile(scope) || contains(scope, use);
|
|
1674
|
+
}
|
|
1675
|
+
function declarationScope(declaration) {
|
|
1676
|
+
if (ts6.isParameter(declaration)) return declaration.parent;
|
|
1677
|
+
if (ts6.isCatchClause(declaration.parent)) return declaration.parent.block;
|
|
1678
|
+
const list = declaration.parent;
|
|
1679
|
+
if (isLoopInitializer(list.parent)) return list.parent.statement;
|
|
1680
|
+
const blockScoped = (list.flags & (ts6.NodeFlags.Const | ts6.NodeFlags.Let)) !== 0;
|
|
1681
|
+
let current = list.parent;
|
|
1682
|
+
if (!blockScoped) {
|
|
1683
|
+
while (current.parent && !ts6.isFunctionLike(current) && !ts6.isSourceFile(current))
|
|
1684
|
+
current = current.parent;
|
|
1685
|
+
return current;
|
|
1686
|
+
}
|
|
1687
|
+
while (current.parent && !isLexicalScope(current)) current = current.parent;
|
|
1688
|
+
return current;
|
|
1689
|
+
}
|
|
1690
|
+
function isLoopInitializer(node) {
|
|
1691
|
+
return ts6.isForStatement(node) || ts6.isForInStatement(node) || ts6.isForOfStatement(node);
|
|
1692
|
+
}
|
|
1693
|
+
function isLexicalScope(node) {
|
|
1694
|
+
return ts6.isBlock(node) || ts6.isSourceFile(node) || ts6.isModuleBlock(node) || ts6.isCaseBlock(node) || ts6.isFunctionLike(node);
|
|
1695
|
+
}
|
|
1696
|
+
function templateState(expression) {
|
|
1697
|
+
const value = expression.getText(expression.getSourceFile()).slice(1, -1);
|
|
1698
|
+
return {
|
|
1699
|
+
paths: [value],
|
|
1700
|
+
placeholders: expression.templateSpans.map((span) => span.expression.getText(expression.getSourceFile())),
|
|
1701
|
+
dynamic: [],
|
|
1702
|
+
sourceKinds: ["template_with_placeholders"]
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
function staticState(path11, sourceKind) {
|
|
1706
|
+
return { paths: [path11], placeholders: [], dynamic: [], sourceKinds: [sourceKind] };
|
|
1707
|
+
}
|
|
1708
|
+
function dynamicState(expression, sourceKind = "dynamic_expression") {
|
|
1709
|
+
return {
|
|
1710
|
+
paths: [],
|
|
1711
|
+
placeholders: [],
|
|
1712
|
+
dynamic: [{
|
|
1713
|
+
expression: expression.getText(expression.getSourceFile()),
|
|
1714
|
+
sourceLine: sourceLine(expression)
|
|
1715
|
+
}],
|
|
1716
|
+
sourceKinds: [sourceKind]
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
function mergeStates(states, sourceKind) {
|
|
1720
|
+
return {
|
|
1721
|
+
paths: states.flatMap((state) => state.paths),
|
|
1722
|
+
placeholders: states.flatMap((state) => state.placeholders),
|
|
1723
|
+
dynamic: states.flatMap((state) => state.dynamic),
|
|
1724
|
+
sourceKinds: [sourceKind, ...states.flatMap((state) => state.sourceKinds)]
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
function pathStatus(paths, placeholders3, dynamic) {
|
|
1728
|
+
if (dynamic.length > 0) return "dynamic";
|
|
1729
|
+
if (paths.length > 1) return "ambiguous";
|
|
1730
|
+
if (placeholders3.length > 0) return "dynamic";
|
|
1731
|
+
if (paths.length === 1) return "static";
|
|
1732
|
+
return "unknown";
|
|
1733
|
+
}
|
|
1734
|
+
function normalizedCandidate(value, method) {
|
|
1735
|
+
const invocation = normalizeODataOperationInvocationPath(value);
|
|
1736
|
+
if (invocation?.wasInvocation) return [invocation.normalizedOperationPath];
|
|
1737
|
+
const intent = classifyODataPathIntent(value, method);
|
|
1738
|
+
if (intent.kind.startsWith("entity_")) return [];
|
|
1739
|
+
if (!value.startsWith("/") || value.slice(1).includes("/") || value.includes("?")) return [];
|
|
1740
|
+
return [value];
|
|
1741
|
+
}
|
|
1742
|
+
function lexicalEvidence(expression, use) {
|
|
1743
|
+
if (!ts6.isIdentifier(expression))
|
|
1744
|
+
return { assignmentLines: [], sourceOrderSafe: expression.getStart() < use.getStart() };
|
|
1745
|
+
const binding = resolveBinding(expression, use);
|
|
1746
|
+
if (!binding) return { assignmentLines: [], sourceOrderSafe: false };
|
|
1747
|
+
const assignmentLines = ts6.isVariableDeclaration(binding.declaration) ? reachingExpressions(binding.declaration, use).slice(binding.declaration.initializer ? 1 : 0).map((item) => sourceLine(item.node)) : [];
|
|
1748
|
+
return {
|
|
1749
|
+
declarationLine: sourceLine(binding.declaration),
|
|
1750
|
+
assignmentLines,
|
|
1751
|
+
sourceOrderSafe: true
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
function unwrap(expression) {
|
|
1755
|
+
if (ts6.isAwaitExpression(expression) || ts6.isParenthesizedExpression(expression))
|
|
1756
|
+
return unwrap(expression.expression);
|
|
1757
|
+
if (ts6.isAsExpression(expression) || ts6.isSatisfiesExpression(expression) || ts6.isTypeAssertionExpression(expression))
|
|
1758
|
+
return unwrap(expression.expression);
|
|
1759
|
+
return expression;
|
|
1760
|
+
}
|
|
1761
|
+
function normalizeRawPath(value) {
|
|
1762
|
+
return value.startsWith("/") ? value : `/${value}`;
|
|
1763
|
+
}
|
|
1764
|
+
function isRuntimeIdentifier(value) {
|
|
1765
|
+
return /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(value);
|
|
1766
|
+
}
|
|
1767
|
+
function contains(parent, child) {
|
|
1768
|
+
const source = child.getSourceFile();
|
|
1769
|
+
return child.getStart(source) >= parent.getStart(source) && child.getEnd() <= parent.getEnd();
|
|
1770
|
+
}
|
|
1771
|
+
function sourceLine(node) {
|
|
1772
|
+
const source = node.getSourceFile();
|
|
1773
|
+
return source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
|
|
1774
|
+
}
|
|
1775
|
+
function unique(values) {
|
|
1776
|
+
return [...new Set(values)].sort();
|
|
1777
|
+
}
|
|
1778
|
+
function emptyAnalysis() {
|
|
1779
|
+
return {
|
|
1780
|
+
status: "unknown",
|
|
1781
|
+
candidateRawPaths: [],
|
|
1782
|
+
candidateNormalizedOperationPaths: [],
|
|
1783
|
+
placeholderKeys: [],
|
|
1784
|
+
sourceKind: "missing",
|
|
1785
|
+
dynamicReassignments: [],
|
|
1786
|
+
lexicalScope: { assignmentLines: [], sourceOrderSafe: false }
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// src/parsers/imported-wrapper-parser.ts
|
|
1556
1791
|
async function parseImportedWrapperCalls(repoPath, filePath, source, serviceBindings) {
|
|
1557
1792
|
const imports = await importsFor(repoPath, filePath, source);
|
|
1558
1793
|
const importedByLocal = new Map(imports.filter((item) => item.sourceFile).map((item) => [item.localName, item]));
|
|
@@ -1560,7 +1795,7 @@ async function parseImportedWrapperCalls(repoPath, filePath, source, serviceBind
|
|
|
1560
1795
|
const out = [];
|
|
1561
1796
|
const cache = /* @__PURE__ */ new Map();
|
|
1562
1797
|
for (const call of calls) {
|
|
1563
|
-
if (!
|
|
1798
|
+
if (!ts7.isIdentifier(call.expression)) continue;
|
|
1564
1799
|
const imported = importedByLocal.get(call.expression.text);
|
|
1565
1800
|
if (!imported?.sourceFile) continue;
|
|
1566
1801
|
const spec = await loadWrapperSpec(repoPath, imported, cache, 0);
|
|
@@ -1572,8 +1807,8 @@ async function parseImportedWrapperCalls(repoPath, filePath, source, serviceBind
|
|
|
1572
1807
|
function collectImportedCalls(source, imports) {
|
|
1573
1808
|
const calls = [];
|
|
1574
1809
|
const visit = (node) => {
|
|
1575
|
-
if (
|
|
1576
|
-
|
|
1810
|
+
if (ts7.isCallExpression(node) && ts7.isIdentifier(node.expression) && imports.has(node.expression.text)) calls.push(node);
|
|
1811
|
+
ts7.forEachChild(node, visit);
|
|
1577
1812
|
};
|
|
1578
1813
|
visit(source);
|
|
1579
1814
|
return calls;
|
|
@@ -1600,20 +1835,20 @@ function directSendSpec(source, sourceFile, name, fn) {
|
|
|
1600
1835
|
const params = parameterNames(fn);
|
|
1601
1836
|
const sends = [];
|
|
1602
1837
|
visitFunctionBody(fn, (node) => {
|
|
1603
|
-
if (
|
|
1838
|
+
if (ts7.isCallExpression(node) && ts7.isPropertyAccessExpression(node.expression) && node.expression.name.text === "send") sends.push(node);
|
|
1604
1839
|
});
|
|
1605
1840
|
if (sends.length !== 1) return void 0;
|
|
1606
1841
|
const send = sends[0];
|
|
1607
|
-
const receiver = send &&
|
|
1842
|
+
const receiver = send && ts7.isPropertyAccessExpression(send.expression) && ts7.isIdentifier(send.expression.expression) ? send.expression.expression.text : void 0;
|
|
1608
1843
|
const object = send?.arguments[0];
|
|
1609
|
-
if (!receiver || !object || !
|
|
1844
|
+
if (!receiver || !object || !ts7.isObjectLiteralExpression(object)) return void 0;
|
|
1610
1845
|
const pathExpr = propertyExpression(object, "path");
|
|
1611
1846
|
const methodExpr = propertyExpression(object, "method");
|
|
1612
|
-
const pathName = pathExpr &&
|
|
1847
|
+
const pathName = pathExpr && ts7.isIdentifier(pathExpr) ? pathExpr.text : void 0;
|
|
1613
1848
|
const clientIndex = params.indexOf(receiver);
|
|
1614
1849
|
const pathIndex = pathName ? params.indexOf(pathName) : -1;
|
|
1615
1850
|
if (clientIndex < 0 || pathIndex < 0) return void 0;
|
|
1616
|
-
const methodName = methodExpr &&
|
|
1851
|
+
const methodName = methodExpr && ts7.isIdentifier(methodExpr) ? methodExpr.text : void 0;
|
|
1617
1852
|
const methodIndex = methodName ? params.indexOf(methodName) : -1;
|
|
1618
1853
|
return { clientIndex, pathIndex, methodIndex: methodIndex >= 0 ? methodIndex : void 0, methodLiteral: literal(methodExpr), sourceFile, sourceLine: lineOf3(source, fn), chain: [name] };
|
|
1619
1854
|
}
|
|
@@ -1622,11 +1857,11 @@ async function nestedSendSpec(repoPath, sourceFile, source, name, fn, cache, dep
|
|
|
1622
1857
|
const byLocal = new Map(imports.filter((item) => item.sourceFile).map((item) => [item.localName, item]));
|
|
1623
1858
|
const calls = [];
|
|
1624
1859
|
visitFunctionBody(fn, (node) => {
|
|
1625
|
-
if (
|
|
1860
|
+
if (ts7.isCallExpression(node) && ts7.isIdentifier(node.expression) && byLocal.has(node.expression.text)) calls.push(node);
|
|
1626
1861
|
});
|
|
1627
1862
|
if (calls.length !== 1) return void 0;
|
|
1628
1863
|
const call = calls[0];
|
|
1629
|
-
const imported = call &&
|
|
1864
|
+
const imported = call && ts7.isIdentifier(call.expression) ? byLocal.get(call.expression.text) : void 0;
|
|
1630
1865
|
const nested = imported ? await loadWrapperSpec(repoPath, imported, cache, depth + 1) : void 0;
|
|
1631
1866
|
if (!call || !nested) return void 0;
|
|
1632
1867
|
const params = parameterNames(fn);
|
|
@@ -1638,12 +1873,12 @@ async function nestedSendSpec(repoPath, sourceFile, source, name, fn, cache, dep
|
|
|
1638
1873
|
}
|
|
1639
1874
|
function wrapperCallFact(source, filePath, call, spec, serviceBindings) {
|
|
1640
1875
|
const client = call.arguments[spec.clientIndex];
|
|
1641
|
-
if (!client || !
|
|
1642
|
-
const pathValue = resolveCallerPath(call.arguments[spec.pathIndex], call);
|
|
1876
|
+
if (!client || !ts7.isIdentifier(client) || !serviceBindings.has(client.text)) return void 0;
|
|
1643
1877
|
const methodValue = spec.methodIndex === void 0 ? spec.methodLiteral : literal(call.arguments[spec.methodIndex]);
|
|
1644
1878
|
const method = (methodValue ?? "POST").toUpperCase();
|
|
1645
|
-
const
|
|
1646
|
-
const operationPathExpr =
|
|
1879
|
+
const pathAnalysis = analyzeOperationPath(call.arguments[spec.pathIndex], call, method);
|
|
1880
|
+
const operationPathExpr = operationPathExpression(pathAnalysis);
|
|
1881
|
+
const unresolvedReason = pathUnresolvedReason(pathAnalysis);
|
|
1647
1882
|
return {
|
|
1648
1883
|
callType: "remote_action",
|
|
1649
1884
|
serviceVariableName: client.text,
|
|
@@ -1653,59 +1888,41 @@ function wrapperCallFact(source, filePath, call, spec, serviceBindings) {
|
|
|
1653
1888
|
sourceFile: normalizePath(filePath),
|
|
1654
1889
|
sourceLine: lineOf3(source, call),
|
|
1655
1890
|
confidence: operationPathExpr ? 0.85 : 0.5,
|
|
1656
|
-
unresolvedReason
|
|
1891
|
+
unresolvedReason,
|
|
1657
1892
|
evidence: {
|
|
1658
1893
|
parser: "typescript_ast",
|
|
1659
|
-
classifier:
|
|
1894
|
+
classifier: importedWrapperClassifier(pathAnalysis.status),
|
|
1660
1895
|
receiver: client.text,
|
|
1661
1896
|
wrapperFunction: spec.chain[0],
|
|
1662
1897
|
wrapperChain: spec.chain,
|
|
1663
1898
|
callerSite: { sourceFile: normalizePath(filePath), sourceLine: lineOf3(source, call) },
|
|
1664
1899
|
calleeSite: { sourceFile: spec.sourceFile, sourceLine: spec.sourceLine },
|
|
1665
|
-
rawPathExpression:
|
|
1666
|
-
missingPathIdentifier:
|
|
1667
|
-
|
|
1900
|
+
rawPathExpression: pathAnalysis.rawExpression,
|
|
1901
|
+
missingPathIdentifier: pathAnalysis.runtimeIdentifier,
|
|
1902
|
+
pathAnalysis,
|
|
1903
|
+
odataPathIntent: operationPathExpr ? classifyODataPathIntent(operationPathExpr, method) : void 0
|
|
1668
1904
|
}
|
|
1669
1905
|
};
|
|
1670
1906
|
}
|
|
1671
|
-
function
|
|
1672
|
-
if (
|
|
1673
|
-
if (
|
|
1674
|
-
|
|
1675
|
-
if (ts6.isIdentifier(expr)) {
|
|
1676
|
-
const initializer = constInitializer(expr.text, use);
|
|
1677
|
-
if (initializer) {
|
|
1678
|
-
const resolved2 = resolveCallerPath(initializer, initializer);
|
|
1679
|
-
return { ...resolved2, sourceKind: "const", rawExpression: expr.text };
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
return { sourceKind: "dynamic", rawExpression: expr.getText() };
|
|
1683
|
-
}
|
|
1684
|
-
function constInitializer(name, use) {
|
|
1685
|
-
let found;
|
|
1686
|
-
const source = use.getSourceFile();
|
|
1687
|
-
const visit = (node) => {
|
|
1688
|
-
if (node.getStart(source) >= use.getStart(source)) return;
|
|
1689
|
-
if (ts6.isVariableDeclaration(node) && ts6.isIdentifier(node.name) && node.name.text === name && node.initializer && (node.parent.flags & ts6.NodeFlags.Const) !== 0) found = node.initializer;
|
|
1690
|
-
ts6.forEachChild(node, visit);
|
|
1691
|
-
};
|
|
1692
|
-
visit(source);
|
|
1693
|
-
return found;
|
|
1907
|
+
function importedWrapperClassifier(status) {
|
|
1908
|
+
if (status === "static") return "imported_wrapper_literal_path";
|
|
1909
|
+
if (status === "ambiguous") return "imported_wrapper_ambiguous_path";
|
|
1910
|
+
return "imported_wrapper_dynamic_path";
|
|
1694
1911
|
}
|
|
1695
1912
|
function findFunction(source, exportedName) {
|
|
1696
1913
|
const localName = exportedLocalName(source, exportedName);
|
|
1697
1914
|
for (const statement of source.statements) {
|
|
1698
|
-
if (
|
|
1699
|
-
if (!
|
|
1915
|
+
if (ts7.isFunctionDeclaration(statement) && statement.name?.text === localName) return { name: exportedName, fn: statement };
|
|
1916
|
+
if (!ts7.isVariableStatement(statement)) continue;
|
|
1700
1917
|
for (const declaration of statement.declarationList.declarations) {
|
|
1701
|
-
if (
|
|
1918
|
+
if (ts7.isIdentifier(declaration.name) && declaration.name.text === localName && declaration.initializer && (ts7.isArrowFunction(declaration.initializer) || ts7.isFunctionExpression(declaration.initializer))) return { name: exportedName, fn: declaration.initializer };
|
|
1702
1919
|
}
|
|
1703
1920
|
}
|
|
1704
1921
|
return void 0;
|
|
1705
1922
|
}
|
|
1706
1923
|
function exportedLocalName(source, exportedName) {
|
|
1707
1924
|
for (const statement of source.statements) {
|
|
1708
|
-
if (!
|
|
1925
|
+
if (!ts7.isExportDeclaration(statement) || !statement.exportClause || !ts7.isNamedExports(statement.exportClause)) continue;
|
|
1709
1926
|
const match = statement.exportClause.elements.find((item) => item.name.text === exportedName);
|
|
1710
1927
|
if (match) return match.propertyName?.text ?? match.name.text;
|
|
1711
1928
|
}
|
|
@@ -1713,36 +1930,30 @@ function exportedLocalName(source, exportedName) {
|
|
|
1713
1930
|
}
|
|
1714
1931
|
function visitFunctionBody(fn, visitor) {
|
|
1715
1932
|
const visit = (node) => {
|
|
1716
|
-
if (node !== fn &&
|
|
1933
|
+
if (node !== fn && ts7.isFunctionLike(node)) return;
|
|
1717
1934
|
visitor(node);
|
|
1718
|
-
|
|
1935
|
+
ts7.forEachChild(node, visit);
|
|
1719
1936
|
};
|
|
1720
1937
|
if (fn.body) visit(fn.body);
|
|
1721
1938
|
}
|
|
1722
1939
|
function parameterNames(fn) {
|
|
1723
|
-
return fn.parameters.map((parameter) =>
|
|
1940
|
+
return fn.parameters.map((parameter) => ts7.isIdentifier(parameter.name) ? parameter.name.text : "");
|
|
1724
1941
|
}
|
|
1725
1942
|
function mappedParameterIndex(expr, parameters) {
|
|
1726
|
-
return expr &&
|
|
1943
|
+
return expr && ts7.isIdentifier(expr) ? parameters.indexOf(expr.text) : -1;
|
|
1727
1944
|
}
|
|
1728
1945
|
function propertyExpression(object, key) {
|
|
1729
1946
|
for (const property of object.properties) {
|
|
1730
|
-
if (
|
|
1731
|
-
if (
|
|
1947
|
+
if (ts7.isPropertyAssignment(property) && propertyName(property.name) === key) return property.initializer;
|
|
1948
|
+
if (ts7.isShorthandPropertyAssignment(property) && property.name.text === key) return property.name;
|
|
1732
1949
|
}
|
|
1733
1950
|
return void 0;
|
|
1734
1951
|
}
|
|
1735
1952
|
function propertyName(name) {
|
|
1736
|
-
return
|
|
1953
|
+
return ts7.isIdentifier(name) || ts7.isStringLiteralLike(name) ? name.text : void 0;
|
|
1737
1954
|
}
|
|
1738
1955
|
function literal(expr) {
|
|
1739
|
-
return expr && (
|
|
1740
|
-
}
|
|
1741
|
-
function normalizeOperationPath(value) {
|
|
1742
|
-
return value.startsWith("/") ? value : `/${value}`;
|
|
1743
|
-
}
|
|
1744
|
-
function runtimePathExpression(value) {
|
|
1745
|
-
return value ? `\${${value}}` : void 0;
|
|
1956
|
+
return expr && (ts7.isStringLiteralLike(expr) || ts7.isNoSubstitutionTemplateLiteral(expr)) ? expr.text : void 0;
|
|
1746
1957
|
}
|
|
1747
1958
|
|
|
1748
1959
|
// src/parsers/outbound-call-parser.ts
|
|
@@ -1751,47 +1962,47 @@ function lineOf4(text, idx) {
|
|
|
1751
1962
|
}
|
|
1752
1963
|
function entityFromExpression(expr) {
|
|
1753
1964
|
if (!expr) return void 0;
|
|
1754
|
-
if (
|
|
1755
|
-
if (
|
|
1756
|
-
if (
|
|
1965
|
+
if (ts8.isIdentifier(expr) || ts8.isStringLiteral(expr) || ts8.isNoSubstitutionTemplateLiteral(expr)) return expr.text;
|
|
1966
|
+
if (ts8.isPropertyAccessExpression(expr) && expr.expression.kind === ts8.SyntaxKind.ThisKeyword) return expr.name.text;
|
|
1967
|
+
if (ts8.isElementAccessExpression(expr) && expr.argumentExpression && (ts8.isStringLiteral(expr.argumentExpression) || ts8.isNoSubstitutionTemplateLiteral(expr.argumentExpression))) return expr.argumentExpression.text;
|
|
1757
1968
|
return void 0;
|
|
1758
1969
|
}
|
|
1759
1970
|
function expressionName(expr) {
|
|
1760
|
-
if (
|
|
1761
|
-
if (
|
|
1971
|
+
if (ts8.isIdentifier(expr)) return expr.text;
|
|
1972
|
+
if (ts8.isPropertyAccessExpression(expr)) return `${expressionName(expr.expression)}.${expr.name.text}`;
|
|
1762
1973
|
return expr.getText();
|
|
1763
1974
|
}
|
|
1764
1975
|
function variableInitializers(source) {
|
|
1765
1976
|
const initializers = /* @__PURE__ */ new Map();
|
|
1766
1977
|
for (const statement of source.statements) {
|
|
1767
|
-
if (!
|
|
1978
|
+
if (!ts8.isVariableStatement(statement) || (statement.declarationList.flags & ts8.NodeFlags.Const) === 0) continue;
|
|
1768
1979
|
for (const declaration of statement.declarationList.declarations) {
|
|
1769
|
-
if (
|
|
1980
|
+
if (ts8.isIdentifier(declaration.name) && declaration.initializer) initializers.set(declaration.name.text, declaration.initializer);
|
|
1770
1981
|
}
|
|
1771
1982
|
}
|
|
1772
1983
|
return initializers;
|
|
1773
1984
|
}
|
|
1774
1985
|
function queryEntityFromAst(expr, initializers = /* @__PURE__ */ new Map()) {
|
|
1775
|
-
if (
|
|
1776
|
-
if (
|
|
1777
|
-
if (
|
|
1986
|
+
if (ts8.isParenthesizedExpression(expr) || ts8.isAwaitExpression(expr)) return queryEntityFromAst(expr.expression, initializers);
|
|
1987
|
+
if (ts8.isIdentifier(expr) && initializers.has(expr.text)) return queryEntityFromAst(initializers.get(expr.text), initializers);
|
|
1988
|
+
if (ts8.isCallExpression(expr)) {
|
|
1778
1989
|
const name = expressionName(expr.expression);
|
|
1779
1990
|
if (name === "cds.run") return queryEntityFromAst(expr.arguments[0], initializers);
|
|
1780
1991
|
if (["SELECT.one.from", "SELECT.from", "SELECT.one", "INSERT.into", "UPSERT.into", "DELETE.from", "UPDATE.entity"].includes(name)) return entityFromExpression(expr.arguments[0]);
|
|
1781
1992
|
if (name === "UPDATE") return entityFromExpression(expr.arguments[0]);
|
|
1782
|
-
const receiver =
|
|
1993
|
+
const receiver = ts8.isPropertyAccessExpression(expr.expression) ? expr.expression.expression : void 0;
|
|
1783
1994
|
if (receiver) return queryEntityFromAst(receiver, initializers);
|
|
1784
1995
|
}
|
|
1785
1996
|
return void 0;
|
|
1786
1997
|
}
|
|
1787
1998
|
function extractQueryEntity(expr) {
|
|
1788
|
-
const source =
|
|
1999
|
+
const source = ts8.createSourceFile("query.ts", `const __query = (${expr});`, ts8.ScriptTarget.Latest, true, ts8.ScriptKind.TS);
|
|
1789
2000
|
const initializers = variableInitializers(source);
|
|
1790
2001
|
let found;
|
|
1791
2002
|
const visit = (node) => {
|
|
1792
2003
|
if (found) return;
|
|
1793
|
-
if (
|
|
1794
|
-
|
|
2004
|
+
if (ts8.isParenthesizedExpression(node)) found = queryEntityFromAst(node.expression, initializers);
|
|
2005
|
+
ts8.forEachChild(node, visit);
|
|
1795
2006
|
};
|
|
1796
2007
|
visit(source);
|
|
1797
2008
|
return found;
|
|
@@ -1805,7 +2016,7 @@ function parserEvidence(source, node, extra) {
|
|
|
1805
2016
|
return { parser: "typescript_ast", startOffset: node.getStart(source), endOffset: node.getEnd(), ...extra };
|
|
1806
2017
|
}
|
|
1807
2018
|
function isStringLike(expr) {
|
|
1808
|
-
return Boolean(expr && (
|
|
2019
|
+
return Boolean(expr && (ts8.isStringLiteral(expr) || ts8.isNoSubstitutionTemplateLiteral(expr)));
|
|
1809
2020
|
}
|
|
1810
2021
|
function literalText(expr) {
|
|
1811
2022
|
if (isStringLike(expr)) return expr.text;
|
|
@@ -1813,151 +2024,118 @@ function literalText(expr) {
|
|
|
1813
2024
|
}
|
|
1814
2025
|
function objectPropertyText(object, key) {
|
|
1815
2026
|
const prop = object.properties.find(
|
|
1816
|
-
(property) =>
|
|
2027
|
+
(property) => ts8.isPropertyAssignment(property) && nameOfProperty(property.name) === key || ts8.isShorthandPropertyAssignment(property) && property.name.text === key
|
|
1817
2028
|
);
|
|
1818
2029
|
if (!prop) return void 0;
|
|
1819
|
-
return
|
|
2030
|
+
return ts8.isShorthandPropertyAssignment(prop) ? prop.name.text : prop.initializer.getText();
|
|
1820
2031
|
}
|
|
1821
2032
|
function objectPropertyIsShorthand(object, key) {
|
|
1822
|
-
return object.properties.some((property) =>
|
|
2033
|
+
return object.properties.some((property) => ts8.isShorthandPropertyAssignment(property) && property.name.text === key);
|
|
1823
2034
|
}
|
|
1824
2035
|
function nameOfProperty(name) {
|
|
1825
|
-
if (
|
|
2036
|
+
if (ts8.isIdentifier(name) || ts8.isStringLiteral(name) || ts8.isNumericLiteral(name)) return name.text;
|
|
1826
2037
|
return void 0;
|
|
1827
2038
|
}
|
|
1828
|
-
var
|
|
2039
|
+
var maxAliasDepth2 = 5;
|
|
1829
2040
|
function safeRaw(expr) {
|
|
1830
|
-
if (
|
|
2041
|
+
if (ts8.isStringLiteral(expr) || ts8.isNoSubstitutionTemplateLiteral(expr) || ts8.isIdentifier(expr) || ts8.isTemplateExpression(expr)) return expr.getText(expr.getSourceFile());
|
|
1831
2042
|
return void 0;
|
|
1832
2043
|
}
|
|
1833
2044
|
function placeholders2(expr) {
|
|
1834
2045
|
return expr.templateSpans.map((span) => span.expression.getText(expr.getSourceFile()));
|
|
1835
2046
|
}
|
|
1836
2047
|
function isFunctionLikeScope(node) {
|
|
1837
|
-
return
|
|
2048
|
+
return ts8.isFunctionLike(node) || ts8.isSourceFile(node);
|
|
1838
2049
|
}
|
|
1839
2050
|
function nodeContains(parent, child) {
|
|
1840
2051
|
const source = child.getSourceFile();
|
|
1841
2052
|
return child.getStart(source) >= parent.getStart(source) && child.getEnd() <= parent.getEnd();
|
|
1842
2053
|
}
|
|
1843
|
-
function
|
|
1844
|
-
if (
|
|
1845
|
-
if (
|
|
2054
|
+
function declarationScope2(node) {
|
|
2055
|
+
if (ts8.isParameter(node)) return node.parent;
|
|
2056
|
+
if (ts8.isCatchClause(node.parent) && node.parent.variableDeclaration === node) return node.parent;
|
|
1846
2057
|
const list = node.parent;
|
|
1847
|
-
const blockScoped = (list.flags & (
|
|
2058
|
+
const blockScoped = (list.flags & (ts8.NodeFlags.Const | ts8.NodeFlags.Let)) !== 0;
|
|
1848
2059
|
let current = list.parent;
|
|
1849
2060
|
if (!blockScoped) {
|
|
1850
2061
|
while (current.parent && !isFunctionLikeScope(current)) current = current.parent;
|
|
1851
2062
|
return current;
|
|
1852
2063
|
}
|
|
1853
|
-
while (current.parent && !
|
|
2064
|
+
while (current.parent && !ts8.isBlock(current) && !ts8.isSourceFile(current) && !ts8.isModuleBlock(current) && !ts8.isCaseBlock(current) && !isLoopInitializerScope(node, current) && !isFunctionLikeScope(current)) current = current.parent;
|
|
1854
2065
|
return current;
|
|
1855
2066
|
}
|
|
1856
2067
|
function isLoopInitializerScope(declaration, scope) {
|
|
1857
2068
|
const list = declaration.parent;
|
|
1858
|
-
return
|
|
2069
|
+
return ts8.isForStatement(scope) && scope.initializer === list || (ts8.isForInStatement(scope) || ts8.isForOfStatement(scope)) && scope.initializer === list;
|
|
1859
2070
|
}
|
|
1860
2071
|
function catchBindingScope(declaration) {
|
|
1861
|
-
if (
|
|
1862
|
-
return
|
|
2072
|
+
if (ts8.isParameter(declaration)) return void 0;
|
|
2073
|
+
return ts8.isCatchClause(declaration.parent) && declaration.parent.variableDeclaration === declaration ? declaration.parent : void 0;
|
|
1863
2074
|
}
|
|
1864
2075
|
function isAccessibleDeclaration(declaration, use) {
|
|
1865
2076
|
const source = use.getSourceFile();
|
|
1866
2077
|
if (declaration.name.getStart(source) >= use.getStart(source)) return false;
|
|
1867
2078
|
const catchScope = catchBindingScope(declaration);
|
|
1868
2079
|
if (catchScope) return nodeContains(catchScope.block, use);
|
|
1869
|
-
const scope =
|
|
1870
|
-
if (
|
|
1871
|
-
return
|
|
2080
|
+
const scope = declarationScope2(declaration);
|
|
2081
|
+
if (ts8.isForStatement(scope) || ts8.isForInStatement(scope) || ts8.isForOfStatement(scope)) return nodeContains(scope.statement, use);
|
|
2082
|
+
return ts8.isSourceFile(scope) || nodeContains(scope, use);
|
|
1872
2083
|
}
|
|
1873
|
-
function
|
|
2084
|
+
function resolveBinding2(identifier, use) {
|
|
1874
2085
|
const source = use.getSourceFile();
|
|
1875
2086
|
let best;
|
|
1876
2087
|
const visit = (node) => {
|
|
1877
|
-
if (
|
|
1878
|
-
if (
|
|
1879
|
-
|
|
2088
|
+
if (ts8.isVariableDeclaration(node) && ts8.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
|
|
2089
|
+
if (ts8.isParameter(node) && ts8.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
|
|
2090
|
+
ts8.forEachChild(node, visit);
|
|
1880
2091
|
};
|
|
1881
2092
|
visit(source);
|
|
1882
2093
|
if (!best) return { immutable: false, evidence: ["binding_not_found"] };
|
|
1883
|
-
const immutable =
|
|
1884
|
-
return { declaration: best, initializer:
|
|
2094
|
+
const immutable = ts8.isVariableDeclaration(best) && (best.parent.flags & ts8.NodeFlags.Const) !== 0;
|
|
2095
|
+
return { declaration: best, initializer: ts8.isVariableDeclaration(best) ? best.initializer : void 0, immutable, evidence: [immutable ? "lexical_const_binding_before_use" : "lexical_mutable_or_parameter_binding"] };
|
|
1885
2096
|
}
|
|
1886
2097
|
function resolveExpression(expr, use, policy, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
1887
2098
|
if (!expr) return { status: "unknown", sourceKind: "dynamic_expression", placeholderKeys: [], evidence: ["expression_missing"] };
|
|
1888
|
-
if (
|
|
1889
|
-
if (
|
|
1890
|
-
if (
|
|
2099
|
+
if (ts8.isStringLiteral(expr)) return { status: "static", sourceKind: "string_literal", value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["string_literal"] };
|
|
2100
|
+
if (ts8.isNoSubstitutionTemplateLiteral(expr)) return { status: "static", sourceKind: "no_substitution_template", value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["no_substitution_template"] };
|
|
2101
|
+
if (ts8.isTemplateExpression(expr)) {
|
|
1891
2102
|
const keys = placeholders2(expr);
|
|
1892
2103
|
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
2104
|
return { status: "dynamic", sourceKind: "template_with_substitutions", placeholderKeys: keys, evidence: ["template_substitutions_not_static_external_target"] };
|
|
1894
2105
|
}
|
|
1895
|
-
if (
|
|
1896
|
-
if (depth >=
|
|
1897
|
-
const binding =
|
|
2106
|
+
if (ts8.isIdentifier(expr)) {
|
|
2107
|
+
if (depth >= maxAliasDepth2) return { status: "unknown", sourceKind: "const_alias", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["alias_depth_exceeded"], constName: expr.text };
|
|
2108
|
+
const binding = resolveBinding2(expr, use);
|
|
1898
2109
|
if (!binding.declaration || !binding.initializer || !binding.immutable) return { status: "dynamic", sourceKind: "dynamic_expression", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: binding.evidence, constName: expr.text };
|
|
1899
2110
|
if (seen.has(binding.declaration)) return { status: "unknown", sourceKind: "const_alias", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["alias_cycle_detected"], constName: expr.text };
|
|
1900
2111
|
seen.add(binding.declaration);
|
|
1901
2112
|
const resolved2 = resolveExpression(binding.initializer, binding.declaration, policy, depth + 1, seen);
|
|
1902
2113
|
return { ...resolved2, sourceKind: "const_alias", rawExpression: safeRaw(expr), constName: expr.text, evidence: [...binding.evidence, ...resolved2.evidence] };
|
|
1903
2114
|
}
|
|
1904
|
-
return { status: "dynamic", sourceKind: "dynamic_expression", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: [`unsupported_${
|
|
2115
|
+
return { status: "dynamic", sourceKind: "dynamic_expression", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: [`unsupported_${ts8.SyntaxKind[expr.kind] ?? "expression"}`] };
|
|
1905
2116
|
}
|
|
1906
2117
|
function staticExpressionText(expr, initializers) {
|
|
1907
2118
|
if (!expr) return void 0;
|
|
1908
2119
|
if (isStringLike(expr)) return expr.text;
|
|
1909
|
-
if (
|
|
2120
|
+
if (ts8.isIdentifier(expr) && initializers.has(expr.text)) return staticExpressionText(initializers.get(expr.text), initializers);
|
|
1910
2121
|
return void 0;
|
|
1911
2122
|
}
|
|
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
2123
|
function operationPathFromStatic(text) {
|
|
1918
2124
|
return text ? `/${stripQuotes(text).replace(/^\//, "")}` : void 0;
|
|
1919
2125
|
}
|
|
1920
|
-
function staticPathCandidates(identifier, call, method) {
|
|
1921
|
-
const binding = resolveBinding(identifier, call);
|
|
1922
|
-
const declaration = binding.declaration;
|
|
1923
|
-
if (!declaration) return void 0;
|
|
1924
|
-
const source = call.getSourceFile();
|
|
1925
|
-
const paths = [];
|
|
1926
|
-
let hasDynamicAssignments = false;
|
|
1927
|
-
const addExpr = (expr, origin) => {
|
|
1928
|
-
const resolved2 = staticPathExpression(expr, origin);
|
|
1929
|
-
if (resolved2.text) paths.push(operationPathFromStatic(resolved2.text) ?? resolved2.text);
|
|
1930
|
-
else hasDynamicAssignments = true;
|
|
1931
|
-
};
|
|
1932
|
-
if (ts7.isVariableDeclaration(declaration)) addExpr(declaration.initializer, declaration);
|
|
1933
|
-
const visit = (node) => {
|
|
1934
|
-
if (node !== source && isFunctionLikeScope(node) && !nodeContains(node, call)) return;
|
|
1935
|
-
if (node.getStart(source) >= call.getStart(source)) return;
|
|
1936
|
-
if (ts7.isBinaryExpression(node) && node.operatorToken.kind === ts7.SyntaxKind.EqualsToken && ts7.isIdentifier(node.left)) {
|
|
1937
|
-
const target = resolveBinding(node.left, node);
|
|
1938
|
-
if (target.declaration === declaration) addExpr(node.right, node);
|
|
1939
|
-
}
|
|
1940
|
-
ts7.forEachChild(node, visit);
|
|
1941
|
-
};
|
|
1942
|
-
visit(source);
|
|
1943
|
-
const candidatePaths = [...new Set(paths)];
|
|
1944
|
-
if (candidatePaths.length === 0) return void 0;
|
|
1945
|
-
const normalizedCandidateOperations = [...new Set(candidatePaths.map((candidate) => classifyODataPathIntent(candidate, method).topLevelOperationName).filter((value) => Boolean(value)))];
|
|
1946
|
-
return { candidatePaths, normalizedCandidateOperations, candidateSourceKind: "static candidates observed before call in lexical scope", candidateIdentifier: identifier.text, hasDynamicAssignments, conservativeReason: hasDynamicAssignments ? "dynamic_assignment_observed" : normalizedCandidateOperations.length > 1 ? "candidate_tie" : void 0 };
|
|
1947
|
-
}
|
|
1948
2126
|
function destinationExpressionShape(expr) {
|
|
1949
2127
|
if (!expr) return void 0;
|
|
1950
|
-
if (
|
|
1951
|
-
if (
|
|
1952
|
-
if (
|
|
1953
|
-
if (
|
|
1954
|
-
if (
|
|
1955
|
-
if (
|
|
1956
|
-
return
|
|
2128
|
+
if (ts8.isIdentifier(expr)) return "identifier";
|
|
2129
|
+
if (ts8.isPropertyAccessExpression(expr) || ts8.isElementAccessExpression(expr)) return "property_read";
|
|
2130
|
+
if (ts8.isCallExpression(expr)) return "function_call";
|
|
2131
|
+
if (ts8.isConditionalExpression(expr)) return "conditional";
|
|
2132
|
+
if (ts8.isBinaryExpression(expr)) return "binary_expression";
|
|
2133
|
+
if (ts8.isTemplateExpression(expr)) return "template_expression";
|
|
2134
|
+
return ts8.SyntaxKind[expr.kind] ?? "expression";
|
|
1957
2135
|
}
|
|
1958
2136
|
function staticConditionalCandidates(expr, initializers) {
|
|
1959
|
-
const resolved2 = expr &&
|
|
1960
|
-
if (!resolved2 || !
|
|
2137
|
+
const resolved2 = expr && ts8.isIdentifier(expr) && initializers.has(expr.text) ? initializers.get(expr.text) : expr;
|
|
2138
|
+
if (!resolved2 || !ts8.isConditionalExpression(resolved2)) return void 0;
|
|
1961
2139
|
const left = staticExpressionText(resolved2.whenTrue, initializers);
|
|
1962
2140
|
const right = staticExpressionText(resolved2.whenFalse, initializers);
|
|
1963
2141
|
if (!left || !right) return void 0;
|
|
@@ -1965,8 +2143,8 @@ function staticConditionalCandidates(expr, initializers) {
|
|
|
1965
2143
|
}
|
|
1966
2144
|
function propertyInitializer(object, key) {
|
|
1967
2145
|
for (const property of object.properties) {
|
|
1968
|
-
if (
|
|
1969
|
-
if (
|
|
2146
|
+
if (ts8.isPropertyAssignment(property) && nameOfProperty(property.name) === key) return property.initializer;
|
|
2147
|
+
if (ts8.isShorthandPropertyAssignment(property) && property.name.text === key) return property.name;
|
|
1970
2148
|
}
|
|
1971
2149
|
return void 0;
|
|
1972
2150
|
}
|
|
@@ -1979,6 +2157,30 @@ function safeOperationName(value) {
|
|
|
1979
2157
|
if (!value || !/^[A-Za-z_$][\w$]*(?:[./][A-Za-z_$][\w$]*)*$/.test(value)) return void 0;
|
|
1980
2158
|
return operationPathFromStatic(value);
|
|
1981
2159
|
}
|
|
2160
|
+
function wrapperSourceKind(sourceKind) {
|
|
2161
|
+
if (sourceKind.includes("const_alias")) return "const";
|
|
2162
|
+
if (sourceKind.includes("template")) return "template";
|
|
2163
|
+
if (sourceKind.includes("string_literal")) return "literal";
|
|
2164
|
+
return sourceKind.includes("conditional") ? "ambiguous" : "dynamic";
|
|
2165
|
+
}
|
|
2166
|
+
function literalPathSource(analysis) {
|
|
2167
|
+
if (analysis.status !== "static") return void 0;
|
|
2168
|
+
if (analysis.sourceKind.includes("const_alias")) return "same_scope_const_initializer";
|
|
2169
|
+
if (analysis.sourceKind.includes("no_substitution_template")) return "template";
|
|
2170
|
+
return analysis.sourceKind.includes("string_literal") ? "literal" : analysis.sourceKind;
|
|
2171
|
+
}
|
|
2172
|
+
function legacyPathCandidates(analysis) {
|
|
2173
|
+
if (analysis.candidateRawPaths.length < 2 && analysis.dynamicReassignments.length === 0)
|
|
2174
|
+
return void 0;
|
|
2175
|
+
return {
|
|
2176
|
+
candidatePaths: analysis.candidateRawPaths,
|
|
2177
|
+
normalizedCandidateOperations: analysis.candidateNormalizedOperationPaths.map((value) => value.replace(/^\//, "")),
|
|
2178
|
+
candidateSourceKind: analysis.sourceKind,
|
|
2179
|
+
candidateIdentifier: analysis.candidateIdentifier,
|
|
2180
|
+
hasDynamicAssignments: analysis.dynamicReassignments.length > 0,
|
|
2181
|
+
conservativeReason: analysis.dynamicReassignments.length > 0 ? "dynamic_assignment_observed" : "candidate_tie"
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
1982
2184
|
function hasTemplatePlaceholder(value) {
|
|
1983
2185
|
return /\$\{|%7B|%7D/i.test(value);
|
|
1984
2186
|
}
|
|
@@ -2003,7 +2205,7 @@ function externalHttpEvidence(node, source) {
|
|
|
2003
2205
|
const exprText = expr.getText(source);
|
|
2004
2206
|
if (exprText === "useOrFetchDestination") {
|
|
2005
2207
|
const objectArg = node.arguments[0];
|
|
2006
|
-
if (objectArg &&
|
|
2208
|
+
if (objectArg && ts8.isObjectLiteralExpression(objectArg)) {
|
|
2007
2209
|
const destination = destinationTargetFromExpression(propertyInitializer(objectArg, "destinationName"), node);
|
|
2008
2210
|
return { externalTarget: destination ?? { kind: "unknown", dynamic: false }, classifier: "sap_destination_lookup", sourceCallShape: "useOrFetchDestination" };
|
|
2009
2211
|
}
|
|
@@ -2011,13 +2213,13 @@ function externalHttpEvidence(node, source) {
|
|
|
2011
2213
|
if (exprText === "executeHttpRequest") {
|
|
2012
2214
|
const destination = destinationTargetFromExpression(node.arguments[0], node);
|
|
2013
2215
|
const config = node.arguments[1];
|
|
2014
|
-
const method = config &&
|
|
2015
|
-
const url = config &&
|
|
2216
|
+
const method = config && ts8.isObjectLiteralExpression(config) ? httpMethodFromObject(config, node) : void 0;
|
|
2217
|
+
const url = config && ts8.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, "url"), node) : { kind: "unknown", dynamic: false };
|
|
2016
2218
|
return { method, externalTarget: destination ? { ...url, destination } : url, classifier: "sap_execute_http_request", sourceCallShape: "executeHttpRequest" };
|
|
2017
2219
|
}
|
|
2018
2220
|
if (exprText === "axios") {
|
|
2019
2221
|
const config = node.arguments[0];
|
|
2020
|
-
if (config &&
|
|
2222
|
+
if (config && ts8.isObjectLiteralExpression(config)) {
|
|
2021
2223
|
const method = httpMethodFromObject(config, node);
|
|
2022
2224
|
return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, "url"), node), classifier: "axios_config_call", sourceCallShape: "axios(config)" };
|
|
2023
2225
|
}
|
|
@@ -2025,10 +2227,10 @@ function externalHttpEvidence(node, source) {
|
|
|
2025
2227
|
}
|
|
2026
2228
|
if (exprText === "fetch") {
|
|
2027
2229
|
const init = node.arguments[1];
|
|
2028
|
-
const method = init &&
|
|
2230
|
+
const method = init && ts8.isObjectLiteralExpression(init) ? httpMethodFromObject(init, node) : void 0;
|
|
2029
2231
|
return { method, externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: "fetch_call", sourceCallShape: "fetch" };
|
|
2030
2232
|
}
|
|
2031
|
-
if (
|
|
2233
|
+
if (ts8.isPropertyAccessExpression(expr) && ["get", "post", "put", "patch", "delete", "head"].includes(expr.name.text) && expr.expression.getText(source) === "axios") {
|
|
2032
2234
|
return { method: expr.name.text.toUpperCase(), externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: "axios_member_call", sourceCallShape: `axios.${expr.name.text}` };
|
|
2033
2235
|
}
|
|
2034
2236
|
return void 0;
|
|
@@ -2036,27 +2238,27 @@ function externalHttpEvidence(node, source) {
|
|
|
2036
2238
|
function collectServiceVariables(source) {
|
|
2037
2239
|
const vars = /* @__PURE__ */ new Set(["cds", "messaging", "messageClient", "eventClient"]);
|
|
2038
2240
|
const visit = (node) => {
|
|
2039
|
-
if (
|
|
2241
|
+
if (ts8.isVariableDeclaration(node) && ts8.isIdentifier(node.name) && node.initializer) {
|
|
2040
2242
|
const text = node.initializer.getText(source);
|
|
2041
2243
|
if (/cds\.connect\.(to|messaging)\s*\(/.test(text)) vars.add(node.name.text);
|
|
2042
2244
|
}
|
|
2043
|
-
|
|
2245
|
+
ts8.forEachChild(node, visit);
|
|
2044
2246
|
};
|
|
2045
2247
|
visit(source);
|
|
2046
2248
|
return vars;
|
|
2047
2249
|
}
|
|
2048
2250
|
function receiverName(expr) {
|
|
2049
|
-
if (
|
|
2050
|
-
if (
|
|
2251
|
+
if (ts8.isIdentifier(expr)) return expr.text;
|
|
2252
|
+
if (ts8.isPropertyAccessExpression(expr)) return expr.getText(sourceOf(expr));
|
|
2051
2253
|
return void 0;
|
|
2052
2254
|
}
|
|
2053
2255
|
function sourceOf(node) {
|
|
2054
2256
|
return node.getSourceFile();
|
|
2055
2257
|
}
|
|
2056
2258
|
function rootReceiverName(expr) {
|
|
2057
|
-
if (
|
|
2058
|
-
if (
|
|
2059
|
-
if (
|
|
2259
|
+
if (ts8.isIdentifier(expr)) return expr.text;
|
|
2260
|
+
if (ts8.isPropertyAccessExpression(expr)) return rootReceiverName(expr.expression);
|
|
2261
|
+
if (ts8.isCallExpression(expr)) return rootReceiverName(expr.expression);
|
|
2060
2262
|
return void 0;
|
|
2061
2263
|
}
|
|
2062
2264
|
function isSupportedEventReceiver(receiver, rootReceiver, serviceVariables) {
|
|
@@ -2073,38 +2275,38 @@ function collectWrapperSpecs(source) {
|
|
|
2073
2275
|
const serviceVariables = collectServiceVariables(source);
|
|
2074
2276
|
const calledNames = /* @__PURE__ */ new Set();
|
|
2075
2277
|
const collectCalls = (node) => {
|
|
2076
|
-
if (
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2278
|
+
if (ts8.isCallExpression(node) && ts8.isIdentifier(node.expression))
|
|
2279
|
+
calledNames.add(node.expression.text);
|
|
2280
|
+
if (ts8.isCallExpression(node) && ts8.isCallExpression(node.expression) && ts8.isIdentifier(node.expression.expression))
|
|
2281
|
+
calledNames.add(node.expression.expression.text);
|
|
2282
|
+
ts8.forEachChild(node, collectCalls);
|
|
2081
2283
|
};
|
|
2082
2284
|
collectCalls(source);
|
|
2083
2285
|
const scanFunction = (name, fn) => {
|
|
2084
|
-
if (!calledNames.has(name)) return;
|
|
2085
|
-
const params = fn.parameters.map((param) =>
|
|
2286
|
+
if (!calledNames.has(name) && !isExportedWrapper(fn)) return;
|
|
2287
|
+
const params = fn.parameters.map((param) => ts8.isIdentifier(param.name) ? param.name.text : void 0);
|
|
2086
2288
|
const sends = [];
|
|
2087
2289
|
const visit = (node) => {
|
|
2088
|
-
if (
|
|
2290
|
+
if (ts8.isCallExpression(node) && ts8.isPropertyAccessExpression(node.expression) && node.expression.name.text === "send" && ts8.isIdentifier(node.expression.expression)) {
|
|
2089
2291
|
const objectArg = node.arguments[0];
|
|
2090
|
-
if (objectArg &&
|
|
2292
|
+
if (objectArg && ts8.isObjectLiteralExpression(objectArg)) {
|
|
2091
2293
|
const pathProp = propertyInitializer(objectArg, "path");
|
|
2092
2294
|
const methodProp = propertyInitializer(objectArg, "method");
|
|
2093
|
-
const pathName = pathProp &&
|
|
2094
|
-
const methodName = methodProp &&
|
|
2295
|
+
const pathName = pathProp && ts8.isIdentifier(pathProp) ? pathProp.text : void 0;
|
|
2296
|
+
const methodName = methodProp && ts8.isIdentifier(methodProp) ? methodProp.text : void 0;
|
|
2095
2297
|
const methodLiteral = resolveExpression(methodProp, node, "literal").value;
|
|
2096
2298
|
if (pathName) sends.push({ client: node.expression.expression.text, path: pathName, method: methodName, methodLiteral, start: node.getStart(source), end: node.getEnd() });
|
|
2097
2299
|
}
|
|
2098
2300
|
}
|
|
2099
|
-
if (
|
|
2301
|
+
if (ts8.isCallExpression(node) && ts8.isIdentifier(node.expression) && specs.has(node.expression.text)) {
|
|
2100
2302
|
const nested = specs.get(node.expression.text);
|
|
2101
2303
|
const pathArg = nested ? node.arguments[nested.pathIndex] : void 0;
|
|
2102
2304
|
const clientArg = nested?.clientIndex === void 0 ? void 0 : node.arguments[nested.clientIndex];
|
|
2103
|
-
const pathName = pathArg &&
|
|
2104
|
-
const clientName = clientArg &&
|
|
2305
|
+
const pathName = pathArg && ts8.isIdentifier(pathArg) ? pathArg.text : void 0;
|
|
2306
|
+
const clientName = clientArg && ts8.isIdentifier(clientArg) ? clientArg.text : nested?.clientName;
|
|
2105
2307
|
if (nested && pathName && clientName) sends.push({ client: clientName, path: pathName, method: nested.methodName, methodLiteral: nested.methodLiteral, nestedWrapperFunction: node.expression.text, start: node.getStart(source), end: node.getEnd() });
|
|
2106
2308
|
}
|
|
2107
|
-
|
|
2309
|
+
ts8.forEachChild(node, visit);
|
|
2108
2310
|
};
|
|
2109
2311
|
visit(fn);
|
|
2110
2312
|
if (sends.length !== 1) return;
|
|
@@ -2116,13 +2318,18 @@ function collectWrapperSpecs(source) {
|
|
|
2116
2318
|
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
2319
|
};
|
|
2118
2320
|
const visitTop = (node) => {
|
|
2119
|
-
if (
|
|
2120
|
-
if (
|
|
2121
|
-
|
|
2321
|
+
if (ts8.isFunctionDeclaration(node) && node.name) scanFunction(node.name.text, node);
|
|
2322
|
+
if (ts8.isVariableDeclaration(node) && ts8.isIdentifier(node.name) && node.initializer && (ts8.isArrowFunction(node.initializer) || ts8.isFunctionExpression(node.initializer))) scanFunction(node.name.text, node.initializer);
|
|
2323
|
+
ts8.forEachChild(node, visitTop);
|
|
2122
2324
|
};
|
|
2123
2325
|
visitTop(source);
|
|
2124
2326
|
return specs;
|
|
2125
2327
|
}
|
|
2328
|
+
function isExportedWrapper(fn) {
|
|
2329
|
+
const declaration = ts8.isFunctionDeclaration(fn) ? fn : ts8.isVariableDeclaration(fn.parent) ? fn.parent.parent.parent : void 0;
|
|
2330
|
+
if (!declaration || !ts8.canHaveModifiers(declaration)) return false;
|
|
2331
|
+
return ts8.getModifiers(declaration)?.some((modifier) => modifier.kind === ts8.SyntaxKind.ExportKeyword) ?? false;
|
|
2332
|
+
}
|
|
2126
2333
|
function classifyOutboundCallsInSource(source, filePath) {
|
|
2127
2334
|
const calls = [];
|
|
2128
2335
|
const sourceFile = normalizePath(filePath);
|
|
@@ -2134,7 +2341,7 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
2134
2341
|
calls.push({ node, fact: { ...fact, sourceFile, sourceLine: lineOf4(source.text, node.getStart(source)), confidence: fact.confidence ?? 0.8, evidence: parserEvidence(source, node, extra) } });
|
|
2135
2342
|
};
|
|
2136
2343
|
const visit = (node) => {
|
|
2137
|
-
if (
|
|
2344
|
+
if (ts8.isCallExpression(node)) {
|
|
2138
2345
|
if (wrapperInternalRanges.some((range) => node.getStart(source) >= range.start && node.getEnd() <= range.end)) {
|
|
2139
2346
|
return;
|
|
2140
2347
|
}
|
|
@@ -2145,24 +2352,23 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
2145
2352
|
const entity = arg ? queryEntityFromAst(arg, initializers) : void 0;
|
|
2146
2353
|
const payload = arg?.getText(source) ?? "";
|
|
2147
2354
|
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 (
|
|
2355
|
+
} else if (ts8.isPropertyAccessExpression(expr) && expr.name.text === "send" && (ts8.isIdentifier(expr.expression) || ts8.isPropertyAccessExpression(expr.expression))) {
|
|
2149
2356
|
const objectArg = node.arguments[0];
|
|
2150
|
-
if (objectArg &&
|
|
2357
|
+
if (objectArg && ts8.isObjectLiteralExpression(objectArg)) {
|
|
2151
2358
|
const receiver = receiverName(expr.expression);
|
|
2152
2359
|
const query = objectPropertyText(objectArg, "query");
|
|
2153
2360
|
const method = stripQuotes(resolveExpression(propertyInitializer(objectArg, "method"), node, "literal").value ?? objectPropertyText(objectArg, "method") ?? "POST");
|
|
2154
2361
|
const pathExpr = propertyInitializer(objectArg, "path") ?? propertyInitializer(objectArg, "event");
|
|
2155
|
-
const
|
|
2156
|
-
const op = pathExpr ?
|
|
2362
|
+
const pathAnalysis = analyzeOperationPath(pathExpr, node, method);
|
|
2363
|
+
const op = pathExpr ? operationPathExpression(pathAnalysis) ?? pathExpr.getText(source) : void 0;
|
|
2157
2364
|
const shorthandPath = objectPropertyIsShorthand(objectArg, "path");
|
|
2158
|
-
const
|
|
2159
|
-
const candidateOperationPath = candidateEvidence2 && !candidateEvidence2.hasDynamicAssignments && candidateEvidence2.normalizedCandidateOperations.length === 1 && candidateEvidence2.candidatePaths.length > 0 ? candidateEvidence2.candidatePaths[0] : void 0;
|
|
2160
|
-
const operationPathExpr = operationPathFromStatic(resolvedPath.text) ?? candidateOperationPath;
|
|
2365
|
+
const operationPathExpr = operationPathExpression(pathAnalysis);
|
|
2161
2366
|
const intent = classifyODataPathIntent(operationPathExpr, method);
|
|
2162
2367
|
const entityCallTypes = { entity_mutation: "remote_entity_mutation", entity_delete: "remote_entity_delete", entity_media: "remote_entity_media", entity_candidate: "remote_entity_candidate" };
|
|
2163
2368
|
const entityCallType = entityCallTypes[intent.kind];
|
|
2164
2369
|
const isODataQueryRead = method.toUpperCase() === "GET" && ["entity_query", "entity_key_read", "entity_navigation_query"].includes(intent.kind);
|
|
2165
|
-
|
|
2370
|
+
const unresolvedReason = !query && pathExpr ? pathUnresolvedReason(pathAnalysis) : void 0;
|
|
2371
|
+
add(node, { callType: query ? "remote_query" : entityCallType ?? (isODataQueryRead ? "remote_query" : "remote_action"), serviceVariableName: receiver, method, operationPathExpr, queryEntity: query ? extractQueryEntity(query) : isODataQueryRead ? intent.entitySegment : void 0, payloadSummary: summarizeExpression(objectArg.getText(source)), confidence: op || query ? 0.8 : 0.4, unresolvedReason }, { receiver, classifier: "service_client_send_object", operationPathExpression: shorthandPath ? op : void 0, rawPathExpression: pathAnalysis.rawExpression, literalPathSource: literalPathSource(pathAnalysis), odataPathIntent: operationPathExpr ? intent : void 0, pathAnalysis, staticPathCandidates: legacyPathCandidates(pathAnalysis), parserWarning: unresolvedReason });
|
|
2166
2372
|
} else {
|
|
2167
2373
|
const receiver = receiverName(expr.expression);
|
|
2168
2374
|
const rootReceiver = rootReceiverName(expr.expression);
|
|
@@ -2171,33 +2377,35 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
2171
2377
|
const pathArg = node.arguments[1];
|
|
2172
2378
|
const supported = method && supportedHttpMethods.has(method);
|
|
2173
2379
|
if (receiver && supported && serviceVariables.has(rootReceiver ?? receiver)) {
|
|
2174
|
-
const
|
|
2175
|
-
const operationPathExpr =
|
|
2380
|
+
const pathAnalysis = analyzeOperationPath(pathArg, node, method);
|
|
2381
|
+
const operationPathExpr = operationPathExpression(pathAnalysis);
|
|
2176
2382
|
const intent = classifyODataPathIntent(operationPathExpr, method);
|
|
2177
|
-
|
|
2383
|
+
const unresolvedReason = pathUnresolvedReason(pathAnalysis);
|
|
2384
|
+
add(node, { callType: "remote_action", serviceVariableName: rootReceiver ?? receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: operationPathExpr ? 0.8 : 0.45, unresolvedReason }, { receiver, rootReceiver, classifier: "service_client_send_method_path", rawPathExpression: pathAnalysis.rawExpression, literalPathSource: literalPathSource(pathAnalysis), odataPathIntent: operationPathExpr ? intent : void 0, pathAnalysis, staticPathCandidates: legacyPathCandidates(pathAnalysis), parserWarning: unresolvedReason });
|
|
2178
2385
|
} else if (receiver && serviceVariables.has(rootReceiver ?? receiver)) {
|
|
2179
2386
|
const operationPathExpr = safeOperationName(firstArg2.value);
|
|
2180
2387
|
add(node, { callType: "remote_action", serviceVariableName: rootReceiver ?? receiver, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: operationPathExpr ? 0.75 : 0.35, unresolvedReason: operationPathExpr ? void 0 : "unsupported_cap_send_signature" }, { receiver, rootReceiver, classifier: operationPathExpr ? "service_client_send_operation_event" : "service_client_send_unsupported_signature", rawOperationExpression: firstArg2.rawExpression, literalOperationSource: firstArg2.value ? firstArg2.sourceKind : void 0, parserWarning: operationPathExpr ? void 0 : "unsupported_cap_send_signature" });
|
|
2181
2388
|
}
|
|
2182
2389
|
}
|
|
2183
|
-
} else if (
|
|
2184
|
-
const wrapperName =
|
|
2185
|
-
const wrapperArgs =
|
|
2390
|
+
} else if (ts8.isCallExpression(expr) && ts8.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text) || ts8.isIdentifier(expr) && wrapperSpecs.has(expr.text)) {
|
|
2391
|
+
const wrapperName = ts8.isIdentifier(expr) ? expr.text : ts8.isCallExpression(expr) && ts8.isIdentifier(expr.expression) ? expr.expression.text : "";
|
|
2392
|
+
const wrapperArgs = ts8.isIdentifier(expr) ? node.arguments : ts8.isCallExpression(expr) ? expr.arguments : node.arguments;
|
|
2186
2393
|
const spec = wrapperSpecs.get(wrapperName);
|
|
2187
2394
|
const clientArg = spec?.clientIndex === void 0 ? void 0 : wrapperArgs[spec.clientIndex];
|
|
2188
2395
|
const pathArg = spec ? wrapperArgs[spec.pathIndex] : void 0;
|
|
2189
2396
|
const methodArg = spec?.methodIndex === void 0 ? void 0 : wrapperArgs[spec.methodIndex];
|
|
2190
|
-
const receiver = clientArg &&
|
|
2397
|
+
const receiver = clientArg && ts8.isIdentifier(clientArg) ? clientArg.text : spec?.clientName;
|
|
2191
2398
|
const method = stripQuotes(resolveExpression(methodArg, node, "literal").value ?? spec?.methodLiteral ?? "POST");
|
|
2192
|
-
const
|
|
2193
|
-
const operationPathExpr =
|
|
2399
|
+
const pathAnalysis = analyzeOperationPath(pathArg, node, method);
|
|
2400
|
+
const operationPathExpr = operationPathExpression(pathAnalysis);
|
|
2194
2401
|
const normalizedOperationPath = operationPathExpr ? classifyODataPathIntent(operationPathExpr, method).topLevelOperationName : void 0;
|
|
2402
|
+
const unresolvedReason = pathUnresolvedReason(pathAnalysis);
|
|
2195
2403
|
if (spec && receiver && operationPathExpr) {
|
|
2196
|
-
add(node, { callType: "remote_action", serviceVariableName: receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.75 }, { receiver, classifier:
|
|
2404
|
+
add(node, { callType: "remote_action", serviceVariableName: receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.75, unresolvedReason }, { receiver, classifier: pathAnalysis.sourceKind.includes("string_literal") ? "higher_order_wrapper_literal_path" : "higher_order_wrapper_static_path", wrapperFunction: wrapperName, nestedWrapperFunction: spec.nestedWrapperFunction, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf4(source.text, node.getStart(source)), wrapperPathSourceKind: wrapperSourceKind(pathAnalysis.sourceKind), rawPathExpression: pathAnalysis.rawExpression, normalizedOperationPath, literalPathSource: pathAnalysis.sourceKind.includes("const_alias") ? "same_scope_const_initializer" : `wrapper_call_${wrapperSourceKind(pathAnalysis.sourceKind)}`, literalCallerArgumentDetected: true, pathAnalysis });
|
|
2197
2405
|
} else if (spec && receiver) {
|
|
2198
|
-
add(node, { callType: "remote_action", serviceVariableName: receiver, method, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.45, unresolvedReason
|
|
2406
|
+
add(node, { callType: "remote_action", serviceVariableName: receiver, method, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.45, unresolvedReason }, { receiver, classifier: pathAnalysis.status === "ambiguous" ? "higher_order_wrapper_ambiguous_path" : "higher_order_wrapper_dynamic_path", wrapperFunction: wrapperName, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf4(source.text, node.getStart(source)), wrapperPathSourceKind: wrapperSourceKind(pathAnalysis.sourceKind), rawPathExpression: pathAnalysis.rawExpression, pathAnalysis, parserWarning: unresolvedReason });
|
|
2199
2407
|
}
|
|
2200
|
-
} else if (
|
|
2408
|
+
} else if (ts8.isPropertyAccessExpression(expr) && ["emit", "publish", "on"].includes(expr.name.text)) {
|
|
2201
2409
|
const receiver = receiverName(expr.expression);
|
|
2202
2410
|
const rootReceiver = rootReceiverName(expr.expression);
|
|
2203
2411
|
if (isSupportedEventReceiver(receiver, rootReceiver, serviceVariables)) {
|
|
@@ -2213,7 +2421,7 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
2213
2421
|
}
|
|
2214
2422
|
}
|
|
2215
2423
|
}
|
|
2216
|
-
|
|
2424
|
+
ts8.forEachChild(node, visit);
|
|
2217
2425
|
};
|
|
2218
2426
|
visit(source);
|
|
2219
2427
|
return calls;
|
|
@@ -2226,21 +2434,21 @@ function containsSupportedOutboundCall(node) {
|
|
|
2226
2434
|
}
|
|
2227
2435
|
async function parseOutboundCalls(repoPath, filePath) {
|
|
2228
2436
|
const text = await fs7.readFile(path10.join(repoPath, filePath), "utf8");
|
|
2229
|
-
const source =
|
|
2437
|
+
const source = ts8.createSourceFile(filePath, text, ts8.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts8.ScriptKind.TS : ts8.ScriptKind.JS);
|
|
2230
2438
|
const bindingNames = new Set((await parseServiceBindings(repoPath, filePath)).map((binding) => binding.variableName));
|
|
2231
2439
|
const importedWrappers = await parseImportedWrapperCalls(repoPath, filePath, source, bindingNames);
|
|
2232
2440
|
return [...classifyOutboundCallsInSource(source, filePath).map((call) => call.fact), ...importedWrappers, ...parseLocalServiceCalls(text, filePath)];
|
|
2233
2441
|
}
|
|
2234
2442
|
function parseLocalServiceCalls(text, filePath) {
|
|
2235
|
-
const source =
|
|
2443
|
+
const source = ts8.createSourceFile(filePath, text, ts8.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts8.ScriptKind.TS : ts8.ScriptKind.JS);
|
|
2236
2444
|
const aliases = /* @__PURE__ */ new Map();
|
|
2237
2445
|
const calls = [];
|
|
2238
2446
|
const visit = (node) => {
|
|
2239
|
-
if (
|
|
2447
|
+
if (ts8.isVariableDeclaration(node) && ts8.isIdentifier(node.name) && node.initializer) {
|
|
2240
2448
|
const origin = serviceLookup(node.initializer, aliases);
|
|
2241
2449
|
if (origin) aliases.set(node.name.text, { ...origin, chain: [...origin.chain, node.name.text] });
|
|
2242
2450
|
}
|
|
2243
|
-
if (
|
|
2451
|
+
if (ts8.isCallExpression(node)) {
|
|
2244
2452
|
const parsed = serviceOperationCall(node, aliases);
|
|
2245
2453
|
if (parsed && parsed.operation !== "entities") calls.push({
|
|
2246
2454
|
callType: "local_service_call",
|
|
@@ -2263,20 +2471,20 @@ function parseLocalServiceCalls(text, filePath) {
|
|
|
2263
2471
|
})
|
|
2264
2472
|
});
|
|
2265
2473
|
}
|
|
2266
|
-
|
|
2474
|
+
ts8.forEachChild(node, visit);
|
|
2267
2475
|
};
|
|
2268
2476
|
visit(source);
|
|
2269
2477
|
return calls;
|
|
2270
2478
|
}
|
|
2271
2479
|
function serviceLookup(expr, aliases) {
|
|
2272
|
-
if (
|
|
2273
|
-
if (
|
|
2274
|
-
if (
|
|
2480
|
+
if (ts8.isIdentifier(expr)) return aliases.get(expr.text);
|
|
2481
|
+
if (ts8.isPropertyAccessExpression(expr) && expr.expression.getText() === "cds.services") return { service: expr.name.text, lookup: expr.getText(), chain: [expr.getText()] };
|
|
2482
|
+
if (ts8.isElementAccessExpression(expr) && expr.expression.getText() === "cds.services" && ts8.isStringLiteral(expr.argumentExpression)) return { service: expr.argumentExpression.text, lookup: expr.getText(), chain: [expr.getText()] };
|
|
2275
2483
|
return void 0;
|
|
2276
2484
|
}
|
|
2277
2485
|
function serviceOperationCall(node, aliases) {
|
|
2278
2486
|
const expr = node.expression;
|
|
2279
|
-
if (!
|
|
2487
|
+
if (!ts8.isPropertyAccessExpression(expr)) return void 0;
|
|
2280
2488
|
const origin = serviceLookup(expr.expression, aliases);
|
|
2281
2489
|
if (!origin) return void 0;
|
|
2282
2490
|
if (expr.name.text === "send") {
|
|
@@ -2741,9 +2949,9 @@ function normalizeDecoratorOperationSignal(value, raw, candidateOperation) {
|
|
|
2741
2949
|
if (stringMatch?.[1]) {
|
|
2742
2950
|
const identifier = stringMatch[1];
|
|
2743
2951
|
const generated = generatedFromConstantName(identifier);
|
|
2744
|
-
const
|
|
2952
|
+
const normalizedCandidate2 = candidateOperation ? normalizedOperationName(candidateOperation) : void 0;
|
|
2745
2953
|
if (generated) return { status: "resolved", operationName: generated, raw: expression };
|
|
2746
|
-
if (
|
|
2954
|
+
if (normalizedCandidate2 && identifier === normalizedCandidate2) return { status: "resolved", operationName: identifier, raw: expression };
|
|
2747
2955
|
return { status: "unsupported", raw: expression, reason: "string_wrapper_identifier_not_resolved" };
|
|
2748
2956
|
}
|
|
2749
2957
|
if (/^[`'"]/.test(expression) && !/[`'"]$/.test(expression)) return { status: "malformed", raw: expression, reason: "unterminated_literal" };
|
|
@@ -2807,7 +3015,36 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
|
2807
3015
|
const operationLikeRemoteEntity = isRemoteEntityCall && Boolean(op) && credibleOperationSignal && (!strongEntitySignal || indexedOperationCandidateCount > 0);
|
|
2808
3016
|
const isOperationCall = operationLikeRemoteEntity || (callType === "remote_action" || callType === "local_service_call" || callType === "remote_query" && Boolean(op));
|
|
2809
3017
|
const resolution = isOperationCall ? resolveOperation(db, { servicePath, operationPath: op, serviceName: call.local_service_name, repoId: callType === "local_service_call" ? Number(call.repo_id) : void 0, alias: applyVariables(call.aliasExpr ?? call.alias, vars), destination: destination ? applyVariables(destination, vars) : void 0, isDynamic, hasExplicitOverride: Object.keys(vars).length > 0 || callType === "local_service_call" }, workspaceId) : { status: "unresolved", candidates: [], reasons: [] };
|
|
2810
|
-
const evidence = {
|
|
3018
|
+
const evidence = {
|
|
3019
|
+
...callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0, normalized, intent),
|
|
3020
|
+
indexedOperationCandidateCount,
|
|
3021
|
+
parserCallType: callType,
|
|
3022
|
+
entityOperationPrecedence: operationPrecedence(
|
|
3023
|
+
callType,
|
|
3024
|
+
intent,
|
|
3025
|
+
indexedOperationCandidateCount,
|
|
3026
|
+
Boolean(resolution.target)
|
|
3027
|
+
)
|
|
3028
|
+
};
|
|
3029
|
+
const pathAnalysis = objectValue(objectJson(call.evidence_json)?.pathAnalysis);
|
|
3030
|
+
if (callType === "remote_action" && pathAnalysis?.status === "ambiguous") {
|
|
3031
|
+
const candidatePaths = Array.isArray(pathAnalysis.candidateRawPaths) ? pathAnalysis.candidateRawPaths : [];
|
|
3032
|
+
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(
|
|
3033
|
+
workspaceId,
|
|
3034
|
+
"UNRESOLVED_EDGE",
|
|
3035
|
+
"ambiguous",
|
|
3036
|
+
"call",
|
|
3037
|
+
String(call.id),
|
|
3038
|
+
"operation_candidates",
|
|
3039
|
+
candidatePaths.join(","),
|
|
3040
|
+
Number(call.confidence ?? 0.5),
|
|
3041
|
+
JSON.stringify(evidence),
|
|
3042
|
+
0,
|
|
3043
|
+
"Ambiguous operation path candidates require explicit disambiguation",
|
|
3044
|
+
generation
|
|
3045
|
+
);
|
|
3046
|
+
return { status: "ambiguous", callType };
|
|
3047
|
+
}
|
|
2811
3048
|
if (isRemoteEntityCall && (resolution.target || resolution.candidates.length > 0 || resolution.status === "dynamic")) {
|
|
2812
3049
|
if (resolution.target) {
|
|
2813
3050
|
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "REMOTE_CALL_RESOLVES_TO_OPERATION", "resolved", "call", String(call.id), "operation", String(resolution.target.operationId), resolution.target.score, JSON.stringify({ ...evidence, operationEntityPrecedence: "indexed_operation_over_parser_entity" }), 0, generation);
|
|
@@ -2853,6 +3090,34 @@ function operationCandidateCount(db, workspaceId, operationPath, operationName)
|
|
|
2853
3090
|
const row = db.prepare(`SELECT COUNT(*) count FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (o.operation_path=? OR o.operation_path=? OR o.operation_name=?)`).get(workspaceId, operationPath, normalizedName ? `/${normalizedName}` : operationPath, normalizedName);
|
|
2854
3091
|
return Number(row?.count ?? 0);
|
|
2855
3092
|
}
|
|
3093
|
+
function operationPrecedence(callType, intent, indexedOperationCandidateCount, resolvedOperation) {
|
|
3094
|
+
if (resolvedOperation) {
|
|
3095
|
+
return {
|
|
3096
|
+
decision: "operation",
|
|
3097
|
+
reason: "indexed_operation_with_strong_service_context",
|
|
3098
|
+
indexedOperationCandidateCount
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
if (callType === "remote_action" && intent.kind === "operation_invocation") {
|
|
3102
|
+
return {
|
|
3103
|
+
decision: "operation_candidate",
|
|
3104
|
+
rejectionReason: indexedOperationCandidateCount > 0 ? "indexed_candidates_lack_unique_strong_service_context" : "no_indexed_operation_candidate",
|
|
3105
|
+
indexedOperationCandidateCount
|
|
3106
|
+
};
|
|
3107
|
+
}
|
|
3108
|
+
if (intent.kind.startsWith("entity_")) {
|
|
3109
|
+
return {
|
|
3110
|
+
decision: "entity",
|
|
3111
|
+
rejectionReason: indexedOperationCandidateCount > 0 ? "entity_shape_has_precedence_without_resolved_operation_context" : "entity_shape_has_no_indexed_operation_evidence",
|
|
3112
|
+
indexedOperationCandidateCount
|
|
3113
|
+
};
|
|
3114
|
+
}
|
|
3115
|
+
return {
|
|
3116
|
+
decision: "unresolved",
|
|
3117
|
+
rejectionReason: "path_has_no_safe_entity_or_operation_precedence",
|
|
3118
|
+
indexedOperationCandidateCount
|
|
3119
|
+
};
|
|
3120
|
+
}
|
|
2856
3121
|
function unresolvedOperationReason(resolution) {
|
|
2857
3122
|
if (resolution.status === "dynamic") return `Dynamic target requires runtime variable overrides: ${(resolution.reasons.length ? resolution.reasons : ["missing runtime variables"]).join(", ")}`;
|
|
2858
3123
|
if (resolution.candidates.length === 0) return "No indexed target operation matched";
|
|
@@ -2911,7 +3176,7 @@ function linkImplementations(db, workspaceId, generation) {
|
|
|
2911
3176
|
const duplicateFamilies = duplicatePackageFamilies(accepted);
|
|
2912
3177
|
const duplicatePackageAmbiguous = duplicateFamilies.length > 0 && !accepted.some(hasDirectOwnershipEvidence);
|
|
2913
3178
|
const selected = duplicatePackageAmbiguous ? accepted : winners;
|
|
2914
|
-
const
|
|
3179
|
+
const unique2 = !duplicatePackageAmbiguous && winners.length === 1 ? winners[0] : void 0;
|
|
2915
3180
|
const ambiguityReasons = duplicatePackageAmbiguous ? ["duplicate_package_name_candidates"] : winners.length > 1 ? ["multiple_equal_score_implementation_candidates"] : [];
|
|
2916
3181
|
const evidence = {
|
|
2917
3182
|
servicePath: operation.servicePath,
|
|
@@ -2925,16 +3190,16 @@ function linkImplementations(db, workspaceId, generation) {
|
|
|
2925
3190
|
candidateFamilies: duplicateFamilies,
|
|
2926
3191
|
candidates: candidates.map((candidate, index) => candidateEvidence(candidate, index + 1))
|
|
2927
3192
|
};
|
|
2928
|
-
const evidenceWithHints =
|
|
3193
|
+
const evidenceWithHints = unique2 ? evidence : { ...evidence, implementationHintSuggestions: implementationHintSuggestions(evidence) };
|
|
2929
3194
|
if (accepted.length === 0) {
|
|
2930
3195
|
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "OPERATION_IMPLEMENTED_BY_HANDLER", "unresolved", "operation", graphId(operation.operationId), "handler_method_candidates", candidates.map((row) => graphId(row.methodId)).join(","), 0, JSON.stringify(evidenceWithHints), 0, "No implementation candidate passed policy", generation);
|
|
2931
3196
|
edgeCount += 1;
|
|
2932
3197
|
unresolvedCount += 1;
|
|
2933
3198
|
continue;
|
|
2934
3199
|
}
|
|
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",
|
|
3200
|
+
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "OPERATION_IMPLEMENTED_BY_HANDLER", unique2 ? "resolved" : "ambiguous", "operation", graphId(operation.operationId), unique2 ? "handler_method" : "handler_method_candidates", unique2 ? graphId(unique2.methodId) : selected.map((row) => graphId(row.methodId)).join(","), unique2 ? 0.95 : 0.5, JSON.stringify(evidenceWithHints), 0, unique2 ? null : "Ambiguous registered handler implementation candidates", generation);
|
|
2936
3201
|
edgeCount += 1;
|
|
2937
|
-
if (
|
|
3202
|
+
if (unique2) resolvedCount += 1;
|
|
2938
3203
|
else ambiguousCount += 1;
|
|
2939
3204
|
}
|
|
2940
3205
|
return { edgeCount, resolvedCount, ambiguousCount, unresolvedCount };
|
|
@@ -3189,11 +3454,12 @@ function baseTraceEvidence(row, call, persistedEvidence, contextualEvidence) {
|
|
|
3189
3454
|
persistedResolution: persistedResolution(row)
|
|
3190
3455
|
};
|
|
3191
3456
|
}
|
|
3192
|
-
function runtimeResolution(db, row, evidence, vars, workspaceId) {
|
|
3457
|
+
function runtimeResolution(db, row, evidence, vars, workspaceId, contextualUnresolvedReason) {
|
|
3193
3458
|
const substituted = evidenceWithRuntimeVariables(evidence, vars);
|
|
3194
3459
|
if (!isRemoteRuntimeCandidate(row, evidence, vars)) {
|
|
3195
|
-
const
|
|
3196
|
-
|
|
3460
|
+
const unresolvedReason2 = contextualUnresolvedReason ?? row.unresolved_reason;
|
|
3461
|
+
const withSections = withEffectiveResolution(substituted, row, unresolvedReason2);
|
|
3462
|
+
return { row, evidence: withSections, unresolvedReason: unresolvedReason2 };
|
|
3197
3463
|
}
|
|
3198
3464
|
const resolution = resolveRuntimeOperation(db, substituted, workspaceId);
|
|
3199
3465
|
if (resolution.target) {
|
|
@@ -3206,6 +3472,9 @@ function runtimeResolution(db, row, evidence, vars, workspaceId) {
|
|
|
3206
3472
|
function runtimeVariableDiagnostic(edges) {
|
|
3207
3473
|
const missing = /* @__PURE__ */ new Set();
|
|
3208
3474
|
for (const edge of edges) {
|
|
3475
|
+
const effective = parseObject(edge.evidence.effectiveResolution);
|
|
3476
|
+
if (!["dynamic", "unresolved", "ambiguous"].includes(String(effective.status ?? "")))
|
|
3477
|
+
continue;
|
|
3209
3478
|
const substitutions = edge.evidence.runtimeSubstitutions;
|
|
3210
3479
|
if (!substitutions || typeof substitutions !== "object" || Array.isArray(substitutions)) continue;
|
|
3211
3480
|
for (const value of Object.values(substitutions))
|
|
@@ -3295,11 +3564,17 @@ function evidenceWithRuntimeVariables(evidence, vars) {
|
|
|
3295
3564
|
function runtimeSubstitutions(evidence, vars) {
|
|
3296
3565
|
const substitutions = {};
|
|
3297
3566
|
for (const key of ["servicePath", "operationPath", "serviceAliasExpr", "serviceAlias", "destination"]) {
|
|
3298
|
-
const substitution = substituteVariables(
|
|
3567
|
+
const substitution = substituteVariables(substitutionValue(evidence, key), vars);
|
|
3299
3568
|
if (substitution.placeholders.length > 0) substitutions[key] = substitution;
|
|
3300
3569
|
}
|
|
3301
3570
|
return substitutions;
|
|
3302
3571
|
}
|
|
3572
|
+
function substitutionValue(evidence, key) {
|
|
3573
|
+
const value = stringValue3(evidence[key]);
|
|
3574
|
+
if (key !== "operationPath") return value;
|
|
3575
|
+
const normalized = normalizeODataOperationInvocationPath(value);
|
|
3576
|
+
return normalized?.wasInvocation ? normalized.normalizedOperationPath : value;
|
|
3577
|
+
}
|
|
3303
3578
|
function isRemoteRuntimeCandidate(row, evidence, vars) {
|
|
3304
3579
|
if (!vars || Object.keys(vars).length === 0) return false;
|
|
3305
3580
|
if (!["dynamic", "ambiguous", "unresolved"].includes(String(row.status ?? ""))) return false;
|
|
@@ -3332,32 +3607,53 @@ function positiveDepth(value) {
|
|
|
3332
3607
|
function operationStartScope(db, repoId, start, hintOptions) {
|
|
3333
3608
|
const requested = normalizeOperation(start.operationPath ?? start.operation);
|
|
3334
3609
|
if (!requested) return void 0;
|
|
3335
|
-
const rows2 = db.prepare(`SELECT o.id operationId,o.operation_name operationName,o.operation_path operationPath,s.service_path servicePath,r.id repoId,r.name repoName
|
|
3610
|
+
const rows2 = db.prepare(`SELECT o.id operationId,o.operation_name operationName,o.operation_path operationPath,o.source_file sourceFile,o.source_line sourceLine,s.service_path servicePath,r.id repoId,r.name repoName
|
|
3336
3611
|
FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
|
|
3337
3612
|
WHERE (? IS NULL OR r.id=?) AND (? IS NULL OR s.service_path=?) AND (o.operation_name=? OR o.operation_path=? OR o.operation_path=?)
|
|
3338
3613
|
ORDER BY r.name,s.service_path,o.operation_name,o.id`).all(repoId, repoId, start.servicePath, start.servicePath, requested, requested, requested.startsWith("/") ? requested : `/${requested}`);
|
|
3339
3614
|
if (rows2.length === 0) return void 0;
|
|
3340
3615
|
const repoCount = new Set(rows2.map((row) => String(row.repoName))).size;
|
|
3341
3616
|
const serviceCount = new Set(rows2.map((row) => `${String(row.repoName)}:${String(row.servicePath)}`)).size;
|
|
3342
|
-
if (!repoId && repoCount > 1)
|
|
3343
|
-
|
|
3344
|
-
if (
|
|
3617
|
+
if (!repoId && repoCount > 1)
|
|
3618
|
+
return { diagnostics: [ambiguousStartDiagnostic(requested, rows2, "Operation trace start matched multiple repositories; add --repo to disambiguate")] };
|
|
3619
|
+
if (!start.servicePath && serviceCount > 1)
|
|
3620
|
+
return { diagnostics: [ambiguousStartDiagnostic(requested, rows2, "Operation trace start matched multiple services; add --service to disambiguate")] };
|
|
3621
|
+
if (rows2.length !== 1)
|
|
3622
|
+
return { diagnostics: [ambiguousStartDiagnostic(requested, rows2, "Operation trace start matched multiple indexed operations")] };
|
|
3345
3623
|
const operationId = String(rows2[0]?.operationId);
|
|
3346
3624
|
const impl = implementationScope(db, operationId);
|
|
3347
|
-
if (impl.edge?.status === "resolved" && impl.files.size > 0) return { files: impl.files, symbols: impl.symbolId ? /* @__PURE__ */ new Set([impl.symbolId]) : void 0, operationId, diagnostics: [] };
|
|
3625
|
+
if (impl.edge?.status === "resolved" && impl.files.size > 0) return { files: impl.files, symbols: impl.symbolId ? /* @__PURE__ */ new Set([impl.symbolId]) : void 0, repoId: impl.repoId, operationId, diagnostics: [] };
|
|
3348
3626
|
const hinted = implementationMethodIdFromHint(impl.edge, hintOptions);
|
|
3349
3627
|
if (hinted.methodId) {
|
|
3350
3628
|
const hintedScope = handlerScope(db, hinted.methodId);
|
|
3351
|
-
if (hintedScope?.files.size) return { files: hintedScope.files, symbols: hintedScope.symbolId ? /* @__PURE__ */ new Set([hintedScope.symbolId]) : void 0, operationId, diagnostics: [] };
|
|
3629
|
+
if (hintedScope?.files.size) return { files: hintedScope.files, symbols: hintedScope.symbolId ? /* @__PURE__ */ new Set([hintedScope.symbolId]) : void 0, repoId: hintedScope.repoId, operationId, diagnostics: [] };
|
|
3352
3630
|
}
|
|
3353
3631
|
if (impl.edge) {
|
|
3354
3632
|
const evidence = parseEvidence(impl.edge.evidence_json);
|
|
3355
3633
|
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 }];
|
|
3634
|
+
const diagnostics = [{ severity: "warning", code: impl.edge.status === "ambiguous" ? "trace_start_ambiguous" : "trace_start_implementation_unresolved", message: `Indexed operation matched but implementation edge is ${String(impl.edge.status ?? "unresolved")}`, resolutionStage: "implementation", resolutionStatus: impl.edge.status === "ambiguous" ? "ambiguous_implementation" : "rejected_implementation", implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, implementationAmbiguityReasons: evidence.ambiguityReasons, implementationRejectionReasons: implementationRejectionReasons(evidence), implementationHintSuggestions: evidence.implementationHintSuggestions, candidates: evidence.candidates }];
|
|
3357
3635
|
return { operationId, diagnostics: hintDiagnostic ? [hintDiagnostic, ...diagnostics] : diagnostics };
|
|
3358
3636
|
}
|
|
3359
3637
|
return { operationId, diagnostics: [{ severity: "warning", code: "trace_start_implementation_unresolved", message: "Indexed operation matched but no implementation candidate exists", resolutionStage: "implementation", resolutionStatus: "operation_without_implementation" }] };
|
|
3360
3638
|
}
|
|
3639
|
+
function implementationRejectionReasons(evidence) {
|
|
3640
|
+
const candidates = Array.isArray(evidence.candidates) ? evidence.candidates.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
|
|
3641
|
+
const reasons = candidates.flatMap((candidate) => Array.isArray(candidate.rejectedReasons) ? candidate.rejectedReasons.filter((reason) => typeof reason === "string") : []);
|
|
3642
|
+
return [...new Set(reasons)].sort();
|
|
3643
|
+
}
|
|
3644
|
+
function ambiguousStartDiagnostic(requested, candidates, message) {
|
|
3645
|
+
const serviceSuggestions = [...new Set(candidates.flatMap((row) => typeof row.servicePath === "string" ? [`--service ${row.servicePath}`] : []))].sort();
|
|
3646
|
+
return {
|
|
3647
|
+
severity: "warning",
|
|
3648
|
+
code: "trace_start_ambiguous",
|
|
3649
|
+
message,
|
|
3650
|
+
normalizedSelectorValue: requested,
|
|
3651
|
+
resolutionStage: "operation",
|
|
3652
|
+
resolutionStatus: "ambiguous_operation",
|
|
3653
|
+
candidates,
|
|
3654
|
+
serviceSuggestions
|
|
3655
|
+
};
|
|
3656
|
+
}
|
|
3361
3657
|
function sourceFilesForStart(db, repoId, start) {
|
|
3362
3658
|
const handler = start.handler;
|
|
3363
3659
|
const operation = normalizeOperation(start.operation ?? start.operationPath);
|
|
@@ -3385,7 +3681,7 @@ function sourceFilesForStart(db, repoId, start) {
|
|
|
3385
3681
|
operation,
|
|
3386
3682
|
operation
|
|
3387
3683
|
);
|
|
3388
|
-
if (rows2.length > 0) return { files: new Set(rows2.map((row) => row.sourceFile).filter(Boolean)), symbols: new Set(rows2.map((row) => Number(row.symbolId)).filter(Boolean)) };
|
|
3684
|
+
if (rows2.length > 0) return { files: new Set(rows2.map((row) => row.sourceFile).filter(Boolean)), symbols: new Set(rows2.map((row) => Number(row.symbolId)).filter(Boolean)), repoId };
|
|
3389
3685
|
if (start.servicePath && operation) {
|
|
3390
3686
|
const implRows = db.prepare(`SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId
|
|
3391
3687
|
FROM cds_services s JOIN cds_operations o ON o.service_id=s.id
|
|
@@ -3394,7 +3690,7 @@ function sourceFilesForStart(db, repoId, start) {
|
|
|
3394
3690
|
JOIN handler_classes hc ON hc.id=hm.handler_class_id
|
|
3395
3691
|
LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id AND sym.source_file=hc.source_file AND sym.name=hm.method_name
|
|
3396
3692
|
WHERE (? IS NULL OR s.repo_id=?) AND s.service_path=? AND (o.operation_path=? OR o.operation_name=?)`).all(repoId, repoId, start.servicePath, operation, operation);
|
|
3397
|
-
if (implRows.length > 0) return { files: new Set(implRows.map((row) => row.sourceFile).filter(Boolean)), symbols: new Set(implRows.map((row) => Number(row.symbolId)).filter(Boolean)) };
|
|
3693
|
+
if (implRows.length > 0) return { files: new Set(implRows.map((row) => row.sourceFile).filter(Boolean)), symbols: new Set(implRows.map((row) => Number(row.symbolId)).filter(Boolean)), repoId };
|
|
3398
3694
|
}
|
|
3399
3695
|
return void 0;
|
|
3400
3696
|
}
|
|
@@ -3414,6 +3710,7 @@ function startScope(db, start, hintOptions) {
|
|
|
3414
3710
|
return { repo, selectorMatched: false };
|
|
3415
3711
|
return {
|
|
3416
3712
|
repo,
|
|
3713
|
+
executionRepoId: sourceScope?.repoId ?? repo?.id,
|
|
3417
3714
|
sourceFiles,
|
|
3418
3715
|
symbolIds: sourceScope?.symbols,
|
|
3419
3716
|
selectorMatched: !terminalOperationScope && (!hasSelector || sourceFiles !== void 0),
|
|
@@ -3560,20 +3857,59 @@ function knownBindingsForCalls(db, calls) {
|
|
|
3560
3857
|
function knownBindingsForScope(db, repoId, symbolIds, files) {
|
|
3561
3858
|
const map = /* @__PURE__ */ new Map();
|
|
3562
3859
|
if (repoId === void 0) return map;
|
|
3563
|
-
const rows2 = db.prepare(`SELECT b.id,b.variable_name variableName,b.alias,b.alias_expr aliasExpr,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.source_file sourceFile,b.source_line sourceLine,req.service_path requireServicePath,req.destination requireDestination
|
|
3860
|
+
const rows2 = db.prepare(`SELECT b.id,b.symbol_id symbolId,b.variable_name variableName,b.alias,b.alias_expr aliasExpr,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.source_file sourceFile,b.source_line sourceLine,req.service_path requireServicePath,req.destination requireDestination
|
|
3564
3861
|
FROM service_bindings b LEFT JOIN cds_requires req ON req.repo_id=b.repo_id AND req.alias=b.alias
|
|
3565
3862
|
WHERE b.repo_id=?`).all(repoId);
|
|
3566
3863
|
for (const row of rows2) {
|
|
3567
3864
|
if (!row.variableName) continue;
|
|
3568
3865
|
if (files && !files.has(String(row.sourceFile))) continue;
|
|
3569
|
-
if (symbolIds && symbolIds.size > 0)
|
|
3570
|
-
|
|
3571
|
-
|
|
3866
|
+
if (symbolIds && symbolIds.size > 0 && row.symbolId != null && !symbolIds.has(Number(row.symbolId))) continue;
|
|
3867
|
+
const candidate = enrichBinding({
|
|
3868
|
+
...row,
|
|
3869
|
+
bindingId: Number(row.id),
|
|
3870
|
+
source: "local_service_binding",
|
|
3871
|
+
calleeReceiver: row.variableName,
|
|
3872
|
+
resolutionStatus: "selected"
|
|
3873
|
+
});
|
|
3874
|
+
const existing = map.get(row.variableName);
|
|
3875
|
+
if (!existing) {
|
|
3876
|
+
map.set(row.variableName, candidate);
|
|
3877
|
+
continue;
|
|
3572
3878
|
}
|
|
3573
|
-
|
|
3879
|
+
const candidates = uniqueBindingCandidates([
|
|
3880
|
+
...existing.bindingCandidates ?? [bindingCandidateEvidence(existing)],
|
|
3881
|
+
bindingCandidateEvidence(candidate)
|
|
3882
|
+
]);
|
|
3883
|
+
map.set(row.variableName, {
|
|
3884
|
+
...candidate,
|
|
3885
|
+
bindingId: void 0,
|
|
3886
|
+
source: "ambiguous_local_service_bindings",
|
|
3887
|
+
resolutionStatus: "ambiguous",
|
|
3888
|
+
bindingCandidates: candidates
|
|
3889
|
+
});
|
|
3574
3890
|
}
|
|
3575
3891
|
return map;
|
|
3576
3892
|
}
|
|
3893
|
+
function bindingCandidateEvidence(binding) {
|
|
3894
|
+
return {
|
|
3895
|
+
bindingId: binding.bindingId,
|
|
3896
|
+
sourceFile: binding.sourceFile,
|
|
3897
|
+
sourceLine: binding.sourceLine,
|
|
3898
|
+
alias: binding.alias,
|
|
3899
|
+
aliasExpr: binding.aliasExpr,
|
|
3900
|
+
destinationExpr: binding.destinationExpr,
|
|
3901
|
+
servicePathExpr: binding.servicePathExpr
|
|
3902
|
+
};
|
|
3903
|
+
}
|
|
3904
|
+
function uniqueBindingCandidates(candidates) {
|
|
3905
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3906
|
+
return candidates.filter((candidate) => {
|
|
3907
|
+
const key = JSON.stringify(candidate);
|
|
3908
|
+
if (seen.has(key)) return false;
|
|
3909
|
+
seen.add(key);
|
|
3910
|
+
return true;
|
|
3911
|
+
});
|
|
3912
|
+
}
|
|
3577
3913
|
function contextForSymbolCall(db, symbolCall, callerBindings) {
|
|
3578
3914
|
const next = /* @__PURE__ */ new Map();
|
|
3579
3915
|
if (callerBindings.size === 0) return next;
|
|
@@ -3623,6 +3959,21 @@ function contextForSymbolCall(db, symbolCall, callerBindings) {
|
|
|
3623
3959
|
}
|
|
3624
3960
|
function contextualRuntimeResolution(db, call, binding, workspaceId, persistedRows = []) {
|
|
3625
3961
|
if (!binding || String(call.call_type) !== "remote_action" || call.operation_path_expr === void 0 || call.operation_path_expr === null) return {};
|
|
3962
|
+
if (binding.resolutionStatus === "ambiguous") {
|
|
3963
|
+
return {
|
|
3964
|
+
evidence: {
|
|
3965
|
+
contextualServiceBindingAttempted: true,
|
|
3966
|
+
contextualBinding: {
|
|
3967
|
+
source: binding.source,
|
|
3968
|
+
status: "tied",
|
|
3969
|
+
candidates: binding.bindingCandidates
|
|
3970
|
+
},
|
|
3971
|
+
contextualResolutionStatus: "ambiguous",
|
|
3972
|
+
contextualCandidateCount: binding.bindingCandidates?.length ?? 0
|
|
3973
|
+
},
|
|
3974
|
+
unresolvedReason: "Ambiguous contextual service binding candidates"
|
|
3975
|
+
};
|
|
3976
|
+
}
|
|
3626
3977
|
const normalized = normalizeODataOperationInvocationPath(String(call.operation_path_expr));
|
|
3627
3978
|
const op = normalized?.normalizedOperationPath ?? (String(call.operation_path_expr).startsWith("/") ? String(call.operation_path_expr) : `/${String(call.operation_path_expr)}`);
|
|
3628
3979
|
const servicePath = binding.effectiveServicePath ?? binding.servicePathExpr ?? binding.requireServicePath;
|
|
@@ -3655,7 +4006,7 @@ function trace(db, start, options) {
|
|
|
3655
4006
|
const edges = [];
|
|
3656
4007
|
const nodes = /* @__PURE__ */ new Map();
|
|
3657
4008
|
const seenEdges = /* @__PURE__ */ new Set();
|
|
3658
|
-
const queue = scope.selectorMatched ? [{ repoId: scope.
|
|
4009
|
+
const queue = scope.selectorMatched ? [{ repoId: scope.executionRepoId, files: scope.sourceFiles, symbolIds: scope.symbolIds, depth: 1, context: /* @__PURE__ */ new Map() }] : [];
|
|
3659
4010
|
if (scope.startOperationId && scope.selectorMatched) {
|
|
3660
4011
|
const op = operationNode(db, scope.startOperationId);
|
|
3661
4012
|
const impl = implementationScope(db, scope.startOperationId);
|
|
@@ -3725,7 +4076,7 @@ function trace(db, start, options) {
|
|
|
3725
4076
|
String(row.evidence_json || "{}")
|
|
3726
4077
|
);
|
|
3727
4078
|
const rawEvidence = baseTraceEvidence(row, call, persistedEvidence, contextual.evidence);
|
|
3728
|
-
const effective = runtimeResolution(db, row, rawEvidence, options.vars, workspaceIdForCall(db, String(call.id)));
|
|
4079
|
+
const effective = runtimeResolution(db, row, rawEvidence, options.vars, workspaceIdForCall(db, String(call.id)), contextual.unresolvedReason);
|
|
3729
4080
|
const evidence = effective.evidence;
|
|
3730
4081
|
const effectiveRow = effective.row;
|
|
3731
4082
|
const targetNode = `${effectiveRow.to_kind}:${effectiveRow.to_id}`;
|
|
@@ -3826,4 +4177,4 @@ export {
|
|
|
3826
4177
|
linkWorkspace,
|
|
3827
4178
|
trace
|
|
3828
4179
|
};
|
|
3829
|
-
//# sourceMappingURL=chunk-
|
|
4180
|
+
//# sourceMappingURL=chunk-XOROZHW4.js.map
|