@saptools/service-flow 0.1.67 → 0.1.69
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 +20 -0
- package/README.md +27 -9
- package/TECHNICAL-NOTE.md +36 -0
- package/dist/chunk-3N3B5KHV.js +19596 -0
- package/dist/chunk-3N3B5KHV.js.map +1 -0
- package/dist/cli.js +2645 -521
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +67 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/001-index-summary.ts +22 -0
- package/src/cli/003-doctor-package-resolution.ts +68 -0
- package/src/cli/doctor.ts +25 -14
- package/src/cli.ts +151 -87
- package/src/db/000-call-fact-repository.ts +499 -340
- package/src/db/001-fact-lifecycle.ts +60 -30
- package/src/db/002-fact-json-inventory.ts +169 -0
- package/src/db/003-current-fact-semantics.ts +699 -0
- package/src/db/004-package-target-invalidation.ts +183 -0
- package/src/db/005-schema-structure.ts +201 -0
- package/src/db/006-relative-symbol-resolution.ts +464 -0
- package/src/db/007-package-fact-semantics.ts +573 -0
- package/src/db/008-relative-fact-semantics.ts +210 -0
- package/src/db/009-binding-fact-semantics.ts +352 -0
- package/src/db/010-package-symbol-surface-semantics.ts +320 -0
- package/src/db/011-symbol-call-semantics.ts +144 -0
- package/src/db/012-binding-reference-proof.ts +268 -0
- package/src/db/013-index-publication-failure.ts +91 -0
- package/src/db/014-binding-helper-provenance.ts +17 -0
- package/src/db/migrations.ts +16 -3
- package/src/db/repositories.ts +130 -6
- package/src/db/schema.ts +4 -2
- package/src/index.ts +12 -0
- package/src/indexer/cds-extension-resolver.ts +27 -3
- package/src/indexer/repository-indexer.ts +135 -13
- package/src/indexer/workspace-indexer.ts +237 -34
- package/src/linker/003-package-import-symbol-resolver.ts +363 -131
- package/src/linker/004-event-subscription-handler-linker.ts +34 -11
- package/src/linker/005-odata-path-structure.ts +371 -0
- package/src/linker/006-event-template-link.ts +72 -0
- package/src/linker/007-call-edge-insertion.ts +568 -0
- package/src/linker/cross-repo-linker.ts +4 -166
- package/src/linker/odata-path-normalizer.ts +273 -180
- package/src/linker/service-resolver.ts +197 -77
- package/src/parsers/000-direct-query-execution.ts +11 -0
- package/src/parsers/002-symbol-import-bindings.ts +516 -0
- package/src/parsers/003-package-public-surface.ts +661 -0
- package/src/parsers/004-fact-identity.ts +108 -0
- package/src/parsers/005-event-subscription-facts.ts +281 -0
- package/src/parsers/006-binding-identity.ts +348 -0
- package/src/parsers/007-source-fact-reconciliation.ts +105 -0
- package/src/parsers/008-package-surface-publication.ts +82 -0
- package/src/parsers/009-symbol-call-facts.ts +528 -0
- package/src/parsers/010-package-public-surface-analysis.ts +352 -0
- package/src/parsers/011-binding-lexical-scope.ts +583 -0
- package/src/parsers/012-package-fact-contract.ts +306 -0
- package/src/parsers/013-executable-body-eligibility.ts +35 -0
- package/src/parsers/014-service-binding-helper-flow.ts +306 -0
- package/src/parsers/015-service-binding-collector.ts +693 -0
- package/src/parsers/016-local-symbol-reference.ts +261 -0
- package/src/parsers/017-symbol-derived-contexts.ts +268 -0
- package/src/parsers/018-package-commonjs-syntax.ts +142 -0
- package/src/parsers/019-binding-assignment-targets.ts +76 -0
- package/src/parsers/020-stable-local-value.ts +217 -0
- package/src/parsers/021-binding-visibility.ts +168 -0
- package/src/parsers/022-outbound-expression-analysis.ts +700 -0
- package/src/parsers/023-outbound-call-classifier.ts +692 -0
- package/src/parsers/operation-path-analysis.ts +6 -1
- package/src/parsers/outbound-call-parser.ts +162 -512
- package/src/parsers/package-json-parser.ts +45 -3
- package/src/parsers/service-binding-parser-helpers.ts +86 -15
- package/src/parsers/service-binding-parser.ts +147 -597
- package/src/parsers/symbol-parser.ts +513 -352
- package/src/trace/002-trace-diagnostics.ts +36 -1
- package/src/trace/007-implementation-start-diagnostic.ts +1 -0
- package/src/trace/011-event-subscriber-traversal.ts +100 -8
- package/src/trace/013-trace-root-scopes.ts +2 -3
- package/src/trace/014-compact-contract.ts +6 -0
- package/src/trace/015-trace-edge-recorder.ts +61 -4
- package/src/trace/016-compact-projector.ts +15 -17
- package/src/trace/019-trace-edge-semantics.ts +6 -10
- package/src/trace/020-compact-field-projection.ts +122 -38
- package/src/trace/021-compact-decision-normalization.ts +171 -0
- package/src/trace/022-trace-fact-preflight.ts +21 -0
- package/src/trace/023-nested-event-scopes.ts +23 -0
- package/src/trace/024-compact-observation-decision.ts +81 -0
- package/src/trace/025-trace-implementation-scope.ts +123 -0
- package/src/trace/026-trace-start-scope.ts +336 -0
- package/src/trace/027-trace-scope-execution.ts +566 -0
- package/src/trace/028-trace-operation-execution.ts +336 -0
- package/src/trace/029-trace-start-implementation.ts +172 -0
- package/src/trace/030-event-runtime-resolution.ts +151 -0
- package/src/trace/031-local-call-expansion.ts +37 -0
- package/src/trace/implementation-hints.ts +9 -6
- package/src/trace/selectors.ts +1 -0
- package/src/trace/trace-engine.ts +122 -624
- package/src/types.ts +57 -0
- package/src/utils/001-placeholders.ts +188 -10
- package/src/version.ts +1 -1
- package/dist/chunk-ZQABU7MR.js +0 -12151
- package/dist/chunk-ZQABU7MR.js.map +0 -1
|
@@ -1,554 +1,204 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import ts from 'typescript';
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import { normalizePath, stripQuotes } from '../utils/path-utils.js';
|
|
7
|
-
import { summarizeExpression } from '../utils/redaction.js';
|
|
8
|
-
import { classifyODataPathIntent } from '../linker/odata-path-normalizer.js';
|
|
9
|
-
import { parseServiceBindings } from './service-binding-parser.js';
|
|
10
|
-
import { parseImportedWrapperCalls } from './imported-wrapper-parser.js';
|
|
4
|
+
import type { OutboundCallFact, ServiceBindingFact } from '../types.js';
|
|
5
|
+
import { normalizePath } from '../utils/path-utils.js';
|
|
11
6
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from './000-direct-query-execution.js';
|
|
7
|
+
classifyOutboundCallsInSource,
|
|
8
|
+
type ClassifiedOutboundCall,
|
|
9
|
+
} from './023-outbound-call-classifier.js';
|
|
16
10
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} from './
|
|
11
|
+
lineOf,
|
|
12
|
+
literalText,
|
|
13
|
+
parserEvidence,
|
|
14
|
+
} from './022-outbound-expression-analysis.js';
|
|
15
|
+
import { parseImportedWrapperCalls } from './imported-wrapper-parser.js';
|
|
16
|
+
import { parseServiceBindings } from './service-binding-parser.js';
|
|
23
17
|
import type { RepositorySourceContext } from './ts-project.js';
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
function queryRunEvidence(
|
|
46
|
-
source: ts.SourceFile,
|
|
47
|
-
argument: ts.Expression | undefined,
|
|
48
|
-
): Record<string, unknown> {
|
|
49
|
-
const root = argument ? queryBuilderRoot(argument) : undefined;
|
|
50
|
-
return {
|
|
51
|
-
classifier: 'cap_query_run_wrapper',
|
|
52
|
-
queryDispatch: 'cds_run_wrapper',
|
|
53
|
-
...(root ? {
|
|
54
|
-
queryRoot: expressionName(root.expression),
|
|
55
|
-
queryRootStartOffset: root.getStart(source),
|
|
56
|
-
queryRootEndOffset: root.getEnd(),
|
|
57
|
-
} : {}),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
function queryWarning(expr: string): string {
|
|
61
|
-
if (/^\s*[`'"]/.test(expr)) return 'raw_sql_or_cql_expression';
|
|
62
|
-
if (/^\s*\w+\s*$/.test(expr)) return 'query_variable_without_static_initializer';
|
|
63
|
-
return 'dynamic_entity_expression';
|
|
64
|
-
}
|
|
65
|
-
export interface ClassifiedOutboundCall {
|
|
66
|
-
fact: OutboundCallFact;
|
|
67
|
-
node: ts.CallExpression;
|
|
68
|
-
}
|
|
69
|
-
function parserEvidence(source: ts.SourceFile, node: ts.CallExpression, extra?: Record<string, unknown>): Record<string, unknown> {
|
|
70
|
-
return { parser: 'typescript_ast', startOffset: node.getStart(source), endOffset: node.getEnd(), ...extra };
|
|
71
|
-
}
|
|
72
|
-
function isStringLike(expr: ts.Expression | undefined): expr is ts.StringLiteral | ts.NoSubstitutionTemplateLiteral {
|
|
73
|
-
return Boolean(expr && (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)));
|
|
74
|
-
}
|
|
75
|
-
function literalText(expr: ts.Expression | undefined): string | undefined {
|
|
76
|
-
if (isStringLike(expr)) return expr.text;
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
const CDS_LIFECYCLE_EVENTS = new Set([
|
|
80
|
-
'bootstrap', 'loaded', 'connect', 'serving', 'served', 'listening', 'shutdown',
|
|
81
|
-
]);
|
|
82
|
-
function objectPropertyIsShorthand(object: ts.ObjectLiteralExpression, key: string): boolean {
|
|
83
|
-
return object.properties.some((property) => ts.isShorthandPropertyAssignment(property) && property.name.text === key);
|
|
84
|
-
}
|
|
85
|
-
function nameOfProperty(name: ts.PropertyName): string | undefined {
|
|
86
|
-
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
|
|
87
|
-
return undefined;
|
|
88
|
-
}
|
|
89
|
-
type ExpressionStatus = 'static' | 'dynamic' | 'ambiguous' | 'unknown';
|
|
90
|
-
type ExpressionSourceKind = 'string_literal' | 'no_substitution_template' | 'template_with_substitutions' | 'const_alias' | 'conditional_candidates' | 'dynamic_expression';
|
|
91
|
-
interface ExpressionResolution { status: ExpressionStatus; sourceKind: ExpressionSourceKind; value?: string; rawExpression?: string; placeholderKeys: string[]; evidence: string[]; constName?: string }
|
|
92
|
-
function safeRaw(expr: ts.Expression): string | undefined {
|
|
93
|
-
if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr) || ts.isIdentifier(expr) || ts.isTemplateExpression(expr)) return expr.getText(expr.getSourceFile());
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
function placeholders(expr: ts.TemplateExpression): string[] {
|
|
97
|
-
return expr.templateSpans.map((span) => span.expression.getText(expr.getSourceFile()));
|
|
98
|
-
}
|
|
99
|
-
function resolveExpression(expr: ts.Expression | undefined, use: ts.Node, policy: 'operation_path' | 'external' | 'literal', depth = 0, seen = new Set<ts.Node>()): ExpressionResolution {
|
|
100
|
-
if (!expr) return { status: 'unknown', sourceKind: 'dynamic_expression', placeholderKeys: [], evidence: ['expression_missing'] };
|
|
101
|
-
if (ts.isStringLiteral(expr)) return { status: 'static', sourceKind: 'string_literal', value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['string_literal'] };
|
|
102
|
-
if (ts.isNoSubstitutionTemplateLiteral(expr)) return { status: 'static', sourceKind: 'no_substitution_template', value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['no_substitution_template'] };
|
|
103
|
-
if (ts.isTemplateExpression(expr)) {
|
|
104
|
-
const keys = placeholders(expr);
|
|
105
|
-
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'] };
|
|
106
|
-
return { status: 'dynamic', sourceKind: 'template_with_substitutions', placeholderKeys: keys, evidence: ['template_substitutions_not_static_external_target'] };
|
|
107
|
-
}
|
|
108
|
-
if (ts.isIdentifier(expr)) {
|
|
109
|
-
if (depth >= maxAliasDepth) return { status: 'unknown', sourceKind: 'const_alias', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['alias_depth_exceeded'], constName: expr.text };
|
|
110
|
-
const binding = resolveBinding(expr, use);
|
|
111
|
-
if (!binding.declaration || !binding.initializer || !binding.immutable) return { status: 'dynamic', sourceKind: 'dynamic_expression', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: binding.evidence, constName: expr.text };
|
|
112
|
-
if (seen.has(binding.declaration)) return { status: 'unknown', sourceKind: 'const_alias', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['alias_cycle_detected'], constName: expr.text };
|
|
113
|
-
seen.add(binding.declaration);
|
|
114
|
-
const resolved = resolveExpression(binding.initializer, binding.declaration, policy, depth + 1, seen);
|
|
115
|
-
return { ...resolved, sourceKind: 'const_alias', rawExpression: safeRaw(expr), constName: expr.text, evidence: [...binding.evidence, ...resolved.evidence] };
|
|
116
|
-
}
|
|
117
|
-
return { status: 'dynamic', sourceKind: 'dynamic_expression', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: [`unsupported_${ts.SyntaxKind[expr.kind] ?? 'expression'}`] };
|
|
118
|
-
}
|
|
119
|
-
function staticExpressionText(expr: ts.Expression | undefined, initializers: Map<string, ts.Expression>): string | undefined {
|
|
120
|
-
if (!expr) return undefined;
|
|
121
|
-
if (isStringLike(expr)) return expr.text;
|
|
122
|
-
if (ts.isIdentifier(expr) && initializers.has(expr.text)) return staticExpressionText(initializers.get(expr.text), initializers);
|
|
123
|
-
return undefined;
|
|
124
|
-
}
|
|
125
|
-
function operationPathFromStatic(text: string | undefined): string | undefined {
|
|
126
|
-
return text ? `/${stripQuotes(text).replace(/^\//, '')}` : undefined;
|
|
127
|
-
}
|
|
128
|
-
function destinationExpressionShape(expr: ts.Expression | undefined): string | undefined {
|
|
129
|
-
if (!expr) return undefined;
|
|
130
|
-
if (ts.isIdentifier(expr)) return 'identifier';
|
|
131
|
-
if (ts.isPropertyAccessExpression(expr) || ts.isElementAccessExpression(expr)) return 'property_read';
|
|
132
|
-
if (ts.isCallExpression(expr)) return 'function_call';
|
|
133
|
-
if (ts.isConditionalExpression(expr)) return 'conditional';
|
|
134
|
-
if (ts.isBinaryExpression(expr)) return 'binary_expression';
|
|
135
|
-
if (ts.isTemplateExpression(expr)) return 'template_expression';
|
|
136
|
-
return ts.SyntaxKind[expr.kind] ?? 'expression';
|
|
137
|
-
}
|
|
138
|
-
function staticConditionalCandidates(expr: ts.Expression | undefined, initializers: Map<string, ts.Expression>): string[] | undefined {
|
|
139
|
-
const resolved = expr && ts.isIdentifier(expr) && initializers.has(expr.text) ? initializers.get(expr.text) : expr;
|
|
140
|
-
if (!resolved || !ts.isConditionalExpression(resolved)) return undefined;
|
|
141
|
-
const left = staticExpressionText(resolved.whenTrue, initializers);
|
|
142
|
-
const right = staticExpressionText(resolved.whenFalse, initializers);
|
|
143
|
-
if (!left || !right) return undefined;
|
|
144
|
-
return [...new Set([left, right])];
|
|
145
|
-
}
|
|
146
|
-
function propertyInitializer(object: ts.ObjectLiteralExpression, key: string): ts.Expression | undefined {
|
|
147
|
-
for (const property of object.properties) {
|
|
148
|
-
if (ts.isPropertyAssignment(property) && nameOfProperty(property.name) === key) return property.initializer;
|
|
149
|
-
if (ts.isShorthandPropertyAssignment(property) && property.name.text === key) return property.name;
|
|
150
|
-
}
|
|
151
|
-
return undefined;
|
|
152
|
-
}
|
|
153
|
-
function httpMethodFromObject(object: ts.ObjectLiteralExpression, use: ts.Node): string | undefined {
|
|
154
|
-
const text = resolveExpression(propertyInitializer(object, 'method'), use, 'literal').value;
|
|
155
|
-
return text ? stripQuotes(text).toUpperCase() : undefined;
|
|
156
|
-
}
|
|
157
|
-
const supportedHttpMethods = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']);
|
|
158
|
-
function safeOperationName(value: string | undefined): string | undefined {
|
|
159
|
-
if (!value || !/^[A-Za-z_$][\w$]*(?:[./][A-Za-z_$][\w$]*)*$/.test(value)) return undefined;
|
|
160
|
-
return operationPathFromStatic(value);
|
|
161
|
-
}
|
|
162
|
-
function wrapperSourceKind(sourceKind: string): string {
|
|
163
|
-
if (sourceKind.includes('const_alias')) return 'const';
|
|
164
|
-
if (sourceKind.includes('template')) return 'template';
|
|
165
|
-
if (sourceKind.includes('string_literal')) return 'literal';
|
|
166
|
-
return sourceKind.includes('conditional') ? 'ambiguous' : 'dynamic';
|
|
167
|
-
}
|
|
168
|
-
function literalPathSource(analysis: OperationPathAnalysis): string | undefined {
|
|
169
|
-
if (analysis.status !== 'static') return undefined;
|
|
170
|
-
if (analysis.sourceKind.includes('const_alias')) return 'same_scope_const_initializer';
|
|
171
|
-
if (analysis.sourceKind.includes('no_substitution_template')) return 'template';
|
|
172
|
-
return analysis.sourceKind.includes('string_literal') ? 'literal' : analysis.sourceKind;
|
|
173
|
-
}
|
|
174
|
-
function legacyPathCandidates(analysis: OperationPathAnalysis): Record<string, unknown> | undefined {
|
|
175
|
-
if (analysis.candidateRawPaths.length < 2 && analysis.dynamicReassignments.length === 0)
|
|
176
|
-
return undefined;
|
|
177
|
-
return {
|
|
178
|
-
candidatePaths: analysis.candidateRawPaths,
|
|
179
|
-
normalizedCandidateOperations: analysis.candidateNormalizedOperationPaths
|
|
180
|
-
.map((value) => value.replace(/^\//, '')),
|
|
181
|
-
candidateSourceKind: analysis.sourceKind,
|
|
182
|
-
candidateIdentifier: analysis.candidateIdentifier,
|
|
183
|
-
hasDynamicAssignments: analysis.dynamicReassignments.length > 0,
|
|
184
|
-
conservativeReason: analysis.dynamicReassignments.length > 0
|
|
185
|
-
? 'dynamic_assignment_observed'
|
|
186
|
-
: 'candidate_tie',
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
function hasTemplatePlaceholder(value: string): boolean { return /\$\{|%7B|%7D/i.test(value); }
|
|
190
|
-
function urlTargetFromExpression(expr: ts.Expression | undefined, use: ts.Node): Record<string, unknown> {
|
|
191
|
-
const resolved = resolveExpression(expr, use, 'external');
|
|
192
|
-
if (resolved.status === 'static' && resolved.value && !hasTemplatePlaceholder(resolved.value)) return { kind: 'static_url', expression: resolved.value, dynamic: false, sourceKind: resolved.sourceKind };
|
|
193
|
-
if (expr) return { kind: 'url_expression', dynamic: true, expression: `${resolved.sourceKind}:${resolved.placeholderKeys.join('|')}`, expressionShape: resolved.sourceKind, placeholderKeys: resolved.placeholderKeys };
|
|
194
|
-
return { kind: 'unknown', dynamic: false };
|
|
195
|
-
}
|
|
196
|
-
function destinationTargetFromExpression(expr: ts.Expression | undefined, use: ts.Node): Record<string, unknown> | undefined {
|
|
197
|
-
const resolved = resolveExpression(expr, use, 'external');
|
|
198
|
-
const text = resolved.value;
|
|
199
|
-
if (resolved.status === 'static' && text && !hasTemplatePlaceholder(text)) return { kind: 'destination', expression: text, dynamic: false, sourceKind: resolved.sourceKind };
|
|
200
|
-
const candidates = staticConditionalCandidates(expr, new Map<string, ts.Expression>());
|
|
201
|
-
if (candidates) return { kind: 'destination', dynamic: true, expressionShape: 'conditional', candidateLiterals: candidates };
|
|
202
|
-
const shape = destinationExpressionShape(expr);
|
|
203
|
-
if (shape) return { kind: 'destination', dynamic: true, expressionShape: shape };
|
|
204
|
-
return undefined;
|
|
205
|
-
}
|
|
206
|
-
function externalHttpEvidence(node: ts.CallExpression, source: ts.SourceFile): { method?: string; externalTarget: Record<string, unknown>; classifier: string; sourceCallShape: string } | undefined {
|
|
207
|
-
const expr = node.expression;
|
|
208
|
-
const exprText = expr.getText(source);
|
|
209
|
-
if (exprText === 'useOrFetchDestination') {
|
|
210
|
-
const objectArg = node.arguments[0];
|
|
211
|
-
if (objectArg && ts.isObjectLiteralExpression(objectArg)) {
|
|
212
|
-
const destination = destinationTargetFromExpression(propertyInitializer(objectArg, 'destinationName'), node);
|
|
213
|
-
return { externalTarget: destination ?? { kind: 'unknown', dynamic: false }, classifier: 'sap_destination_lookup', sourceCallShape: 'useOrFetchDestination' };
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
if (exprText === 'executeHttpRequest') {
|
|
217
|
-
const destination = destinationTargetFromExpression(node.arguments[0], node);
|
|
218
|
-
const config = node.arguments[1];
|
|
219
|
-
const method = config && ts.isObjectLiteralExpression(config) ? httpMethodFromObject(config, node) : undefined;
|
|
220
|
-
const url = config && ts.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, 'url'), node) : { kind: 'unknown', dynamic: false };
|
|
221
|
-
return { method, externalTarget: destination ? { ...url, destination } : url, classifier: 'sap_execute_http_request', sourceCallShape: 'executeHttpRequest' };
|
|
222
|
-
}
|
|
223
|
-
if (exprText === 'axios') {
|
|
224
|
-
const config = node.arguments[0];
|
|
225
|
-
if (config && ts.isObjectLiteralExpression(config)) {
|
|
226
|
-
const method = httpMethodFromObject(config, node);
|
|
227
|
-
return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, 'url'), node), classifier: 'axios_config_call', sourceCallShape: 'axios(config)' };
|
|
228
|
-
}
|
|
229
|
-
return { externalTarget: { kind: 'unknown', dynamic: false }, classifier: 'axios_unknown_call', sourceCallShape: 'axios(...)' };
|
|
230
|
-
}
|
|
231
|
-
if (exprText === 'fetch') {
|
|
232
|
-
const init = node.arguments[1];
|
|
233
|
-
const method = init && ts.isObjectLiteralExpression(init) ? httpMethodFromObject(init, node) : undefined;
|
|
234
|
-
return { method, externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: 'fetch_call', sourceCallShape: 'fetch' };
|
|
235
|
-
}
|
|
236
|
-
if (ts.isPropertyAccessExpression(expr) && ['get','post','put','patch','delete','head'].includes(expr.name.text) && expr.expression.getText(source) === 'axios') {
|
|
237
|
-
return { method: expr.name.text.toUpperCase(), externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: 'axios_member_call', sourceCallShape: `axios.${expr.name.text}` };
|
|
238
|
-
}
|
|
239
|
-
return undefined;
|
|
240
|
-
}
|
|
241
|
-
function collectServiceVariables(source: ts.SourceFile): Set<string> {
|
|
242
|
-
const vars = new Set<string>(['cds', 'messaging', 'messageClient', 'eventClient']);
|
|
243
|
-
const visit = (node: ts.Node): void => {
|
|
244
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
245
|
-
const text = node.initializer.getText(source);
|
|
246
|
-
if (/cds\.connect\.(to|messaging)\s*\(/.test(text)) vars.add(node.name.text);
|
|
247
|
-
}
|
|
248
|
-
ts.forEachChild(node, visit);
|
|
249
|
-
};
|
|
250
|
-
visit(source);
|
|
251
|
-
return vars;
|
|
252
|
-
}
|
|
253
|
-
function receiverName(expr: ts.Expression): string | undefined {
|
|
254
|
-
if (ts.isIdentifier(expr)) return expr.text;
|
|
255
|
-
if (ts.isPropertyAccessExpression(expr)) return expr.getText(sourceOf(expr));
|
|
256
|
-
return undefined;
|
|
257
|
-
}
|
|
258
|
-
function sourceOf(node: ts.Node): ts.SourceFile {
|
|
259
|
-
return node.getSourceFile();
|
|
260
|
-
}
|
|
261
|
-
function rootReceiverName(expr: ts.Expression): string | undefined {
|
|
262
|
-
if (ts.isIdentifier(expr)) return expr.text;
|
|
263
|
-
if (ts.isPropertyAccessExpression(expr)) return rootReceiverName(expr.expression);
|
|
264
|
-
if (ts.isCallExpression(expr)) return rootReceiverName(expr.expression);
|
|
265
|
-
return undefined;
|
|
266
|
-
}
|
|
267
|
-
function isSupportedEventReceiver(receiver: string | undefined, rootReceiver: string | undefined, serviceVariables: Set<string>): boolean {
|
|
268
|
-
const candidate = rootReceiver ?? receiver;
|
|
269
|
-
if (!candidate) return false;
|
|
270
|
-
if (candidate === 'cds') return true;
|
|
271
|
-
if (serviceVariables.has(candidate)) return true;
|
|
272
|
-
if (receiver && serviceVariables.has(receiver)) return true;
|
|
273
|
-
if (/^(srv|service|serviceClient|messaging|messageClient|eventClient)$/.test(candidate)) return true;
|
|
274
|
-
return false;
|
|
275
|
-
}
|
|
276
|
-
interface WrapperSpec { clientIndex?: number; clientName?: string; pathIndex: number; methodIndex?: number; methodName?: string; methodLiteral?: string; nestedWrapperFunction?: string; definitionLine: number; internalStart: number; internalEnd: number }
|
|
277
|
-
function collectWrapperSpecs(source: ts.SourceFile): Map<string, WrapperSpec> {
|
|
278
|
-
const specs = new Map<string, WrapperSpec>();
|
|
279
|
-
const serviceVariables = collectServiceVariables(source);
|
|
280
|
-
const calledNames = new Set<string>();
|
|
281
|
-
const collectCalls = (node: ts.Node): void => {
|
|
282
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression))
|
|
283
|
-
calledNames.add(node.expression.text);
|
|
284
|
-
if (ts.isCallExpression(node) && ts.isCallExpression(node.expression)
|
|
285
|
-
&& ts.isIdentifier(node.expression.expression))
|
|
286
|
-
calledNames.add(node.expression.expression.text);
|
|
287
|
-
ts.forEachChild(node, collectCalls);
|
|
288
|
-
};
|
|
289
|
-
collectCalls(source);
|
|
290
|
-
const scanFunction = (name: string, fn: ts.FunctionLikeDeclaration): void => {
|
|
291
|
-
if (!calledNames.has(name) && !isExportedWrapper(fn)) return;
|
|
292
|
-
const params = fn.parameters.map((param) => ts.isIdentifier(param.name) ? param.name.text : undefined);
|
|
293
|
-
const sends: Array<{ client: string; path: string; method?: string; methodLiteral?: string; nestedWrapperFunction?: string; start: number; end: number }> = [];
|
|
294
|
-
const visit = (node: ts.Node): void => {
|
|
295
|
-
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'send' && ts.isIdentifier(node.expression.expression)) {
|
|
296
|
-
const objectArg = node.arguments[0];
|
|
297
|
-
if (objectArg && ts.isObjectLiteralExpression(objectArg)) {
|
|
298
|
-
const pathProp = propertyInitializer(objectArg, 'path');
|
|
299
|
-
const methodProp = propertyInitializer(objectArg, 'method');
|
|
300
|
-
const pathName = pathProp && ts.isIdentifier(pathProp) ? pathProp.text : undefined;
|
|
301
|
-
const methodName = methodProp && ts.isIdentifier(methodProp) ? methodProp.text : undefined;
|
|
302
|
-
const methodLiteral = resolveExpression(methodProp, node, 'literal').value;
|
|
303
|
-
if (pathName) sends.push({ client: node.expression.expression.text, path: pathName, method: methodName, methodLiteral, start: node.getStart(source), end: node.getEnd() });
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && specs.has(node.expression.text)) {
|
|
307
|
-
const nested = specs.get(node.expression.text);
|
|
308
|
-
const pathArg = nested ? node.arguments[nested.pathIndex] : undefined;
|
|
309
|
-
const clientArg = nested?.clientIndex === undefined ? undefined : node.arguments[nested.clientIndex];
|
|
310
|
-
const pathName = pathArg && ts.isIdentifier(pathArg) ? pathArg.text : undefined;
|
|
311
|
-
const clientName = clientArg && ts.isIdentifier(clientArg) ? clientArg.text : nested?.clientName;
|
|
312
|
-
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() });
|
|
313
|
-
}
|
|
314
|
-
ts.forEachChild(node, visit);
|
|
315
|
-
};
|
|
316
|
-
visit(fn);
|
|
317
|
-
if (sends.length !== 1) return;
|
|
318
|
-
const found = sends[0];
|
|
319
|
-
const clientIndex = params.indexOf(found.client);
|
|
320
|
-
const pathIndex = params.indexOf(found.path);
|
|
321
|
-
const methodIndex = found.method ? params.indexOf(found.method) : -1;
|
|
322
|
-
const capturesKnownClient = serviceVariables.has(found.client) || /^(srv|service|serviceClient|client|.*Client)$/.test(found.client);
|
|
323
|
-
if (pathIndex >= 0 && (clientIndex >= 0 || capturesKnownClient)) specs.set(name, { clientIndex: clientIndex >= 0 ? clientIndex : undefined, clientName: clientIndex >= 0 ? undefined : found.client, pathIndex, methodIndex: methodIndex >= 0 ? methodIndex : undefined, methodName: found.method, methodLiteral: found.methodLiteral, nestedWrapperFunction: found.nestedWrapperFunction, definitionLine: lineOf(source.text, fn.getStart(source)), internalStart: found.start, internalEnd: found.end });
|
|
324
|
-
};
|
|
325
|
-
const visitTop = (node: ts.Node): void => {
|
|
326
|
-
if (ts.isFunctionDeclaration(node) && node.name) scanFunction(node.name.text, node);
|
|
327
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) scanFunction(node.name.text, node.initializer);
|
|
328
|
-
ts.forEachChild(node, visitTop);
|
|
329
|
-
};
|
|
330
|
-
visitTop(source);
|
|
331
|
-
return specs;
|
|
332
|
-
}
|
|
333
|
-
function isExportedWrapper(fn: ts.FunctionLikeDeclaration): boolean {
|
|
334
|
-
const declaration = ts.isFunctionDeclaration(fn)
|
|
335
|
-
? fn
|
|
336
|
-
: ts.isVariableDeclaration(fn.parent)
|
|
337
|
-
? fn.parent.parent.parent
|
|
338
|
-
: undefined;
|
|
339
|
-
if (!declaration || !ts.canHaveModifiers(declaration)) return false;
|
|
340
|
-
return ts.getModifiers(declaration)?.some((modifier) =>
|
|
341
|
-
modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
342
|
-
}
|
|
343
|
-
export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: string): ClassifiedOutboundCall[] {
|
|
344
|
-
const calls: ClassifiedOutboundCall[] = [];
|
|
345
|
-
const sourceFile = normalizePath(filePath);
|
|
346
|
-
const initializers = variableInitializers(source);
|
|
347
|
-
const serviceVariables = collectServiceVariables(source);
|
|
348
|
-
const wrapperSpecs = collectWrapperSpecs(source);
|
|
349
|
-
const wrapperInternalRanges = [...wrapperSpecs.values()].map((spec) => ({ start: spec.internalStart, end: spec.internalEnd }));
|
|
350
|
-
const add = (node: ts.CallExpression, fact: Omit<OutboundCallFact, 'sourceFile' | 'sourceLine' | 'confidence'> & { confidence?: number }, extra?: Record<string, unknown>): void => {
|
|
351
|
-
calls.push({ node, fact: {
|
|
352
|
-
...fact,
|
|
353
|
-
sourceFile,
|
|
354
|
-
sourceLine: lineOf(source.text, node.getStart(source)),
|
|
355
|
-
callSiteStartOffset: node.getStart(source),
|
|
356
|
-
callSiteEndOffset: node.getEnd(),
|
|
357
|
-
confidence: fact.confidence ?? 0.8,
|
|
358
|
-
evidence: parserEvidence(source, node, extra),
|
|
359
|
-
} });
|
|
360
|
-
};
|
|
361
|
-
const visit = (node: ts.Node): void => {
|
|
362
|
-
if (ts.isCallExpression(node)) {
|
|
363
|
-
if (wrapperInternalRanges.some((range) => node.getStart(source) >= range.start && node.getEnd() <= range.end)) {
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
const expr = node.expression;
|
|
367
|
-
const exprText = expr.getText(source);
|
|
368
|
-
const directQuery = directQueryBuilderStatement(node);
|
|
369
|
-
if (exprText === 'cds.run') {
|
|
370
|
-
const arg = node.arguments[0];
|
|
371
|
-
const entity = arg ? queryEntityFromAst(arg, initializers) : undefined;
|
|
372
|
-
const payload = arg?.getText(source) ?? '';
|
|
373
|
-
add(node, { callType: 'local_db_query', queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? undefined : queryWarning(payload) }, queryRunEvidence(source, arg));
|
|
374
|
-
} else if (directQuery) {
|
|
375
|
-
const entity = queryEntityFromAst(directQuery.logicalCall, initializers);
|
|
376
|
-
const payload = directQuery.logicalCall.getText(source);
|
|
377
|
-
add(directQuery.logicalCall, { callType: 'local_db_query', queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? undefined : queryWarning(payload) }, queryBuilderEvidence(source, directQuery));
|
|
378
|
-
} else if (ts.isPropertyAccessExpression(expr) && expr.name.text === 'send' && (ts.isIdentifier(expr.expression) || ts.isPropertyAccessExpression(expr.expression))) {
|
|
379
|
-
const objectArg = node.arguments[0];
|
|
380
|
-
if (objectArg && ts.isObjectLiteralExpression(objectArg)) {
|
|
381
|
-
const receiver = receiverName(expr.expression);
|
|
382
|
-
const queryExpression = propertyInitializer(objectArg, 'query');
|
|
383
|
-
const methodExpression = propertyInitializer(objectArg, 'method');
|
|
384
|
-
const methodResolution = resolveExpression(methodExpression, node, 'literal');
|
|
385
|
-
const method = stripQuotes(methodResolution.value ?? 'POST');
|
|
386
|
-
const dynamicMethodDefaulted = Boolean(methodExpression && methodResolution.value === undefined);
|
|
387
|
-
const pathExpr = propertyInitializer(objectArg, 'path') ?? propertyInitializer(objectArg, 'event');
|
|
388
|
-
const pathAnalysis = analyzeOperationPath(pathExpr, node, method);
|
|
389
|
-
const op = pathExpr ? operationPathExpression(pathAnalysis) ?? pathExpr.getText(source) : undefined;
|
|
390
|
-
const shorthandPath = objectPropertyIsShorthand(objectArg, 'path');
|
|
391
|
-
const operationPathExpr = operationPathExpression(pathAnalysis);
|
|
392
|
-
const intent = classifyODataPathIntent(operationPathExpr, method);
|
|
393
|
-
const entityCallTypes: Record<string, OutboundCallFact['callType']> = { entity_mutation: 'remote_entity_mutation', entity_delete: 'remote_entity_delete', entity_media: 'remote_entity_media', entity_candidate: 'remote_entity_candidate' };
|
|
394
|
-
const entityCallType = entityCallTypes[intent.kind];
|
|
395
|
-
const isODataQueryRead = method.toUpperCase() === 'GET' && ['entity_query', 'entity_key_read', 'entity_navigation_query'].includes(intent.kind);
|
|
396
|
-
const queryEntity = queryExpression
|
|
397
|
-
? queryEntityFromAst(queryExpression, initializers)
|
|
398
|
-
: isODataQueryRead ? intent.entitySegment : undefined;
|
|
399
|
-
const unresolvedReason = queryExpression
|
|
400
|
-
? queryEntity ? undefined : queryWarning(queryExpression.getText(source))
|
|
401
|
-
: pathExpr ? pathUnresolvedReason(pathAnalysis) : undefined;
|
|
402
|
-
add(node, { callType: queryExpression ? 'remote_query' : entityCallType ?? (isODataQueryRead ? 'remote_query' : 'remote_action'), serviceVariableName: receiver, method, operationPathExpr, queryEntity, payloadSummary: summarizeExpression(objectArg.getText(source)), confidence: op || queryExpression ? 0.8 : 0.4, unresolvedReason }, { receiver, classifier: 'service_client_send_object', operationPathExpression: shorthandPath ? op : undefined, rawPathExpression: pathAnalysis.rawExpression, literalPathSource: literalPathSource(pathAnalysis), odataPathIntent: operationPathExpr ? intent : undefined, pathAnalysis, staticPathCandidates: legacyPathCandidates(pathAnalysis), parserWarning: unresolvedReason, ...(dynamicMethodDefaulted ? { dynamicMethodDefaulted: true } : {}) });
|
|
403
|
-
} else {
|
|
404
|
-
const receiver = receiverName(expr.expression);
|
|
405
|
-
const rootReceiver = rootReceiverName(expr.expression);
|
|
406
|
-
const firstArg = resolveExpression(node.arguments[0], node, 'literal');
|
|
407
|
-
const method = firstArg.value?.toUpperCase();
|
|
408
|
-
const pathArg = node.arguments[1];
|
|
409
|
-
const supported = method && supportedHttpMethods.has(method);
|
|
410
|
-
if (receiver && supported && serviceVariables.has(rootReceiver ?? receiver)) {
|
|
411
|
-
const pathAnalysis = analyzeOperationPath(pathArg, node, method);
|
|
412
|
-
const operationPathExpr = operationPathExpression(pathAnalysis);
|
|
413
|
-
const intent = classifyODataPathIntent(operationPathExpr, method);
|
|
414
|
-
const unresolvedReason = pathUnresolvedReason(pathAnalysis);
|
|
415
|
-
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 : undefined, pathAnalysis, staticPathCandidates: legacyPathCandidates(pathAnalysis), parserWarning: unresolvedReason });
|
|
416
|
-
} else if (receiver && serviceVariables.has(rootReceiver ?? receiver)) {
|
|
417
|
-
const operationPathExpr = safeOperationName(firstArg.value);
|
|
418
|
-
add(node, { callType: 'remote_action', serviceVariableName: rootReceiver ?? receiver, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: operationPathExpr ? 0.75 : 0.35, unresolvedReason: operationPathExpr ? undefined : 'unsupported_cap_send_signature' }, { receiver, rootReceiver, classifier: operationPathExpr ? 'service_client_send_operation_event' : 'service_client_send_unsupported_signature', rawOperationExpression: firstArg.rawExpression, literalOperationSource: firstArg.value ? firstArg.sourceKind : undefined, parserWarning: operationPathExpr ? undefined : 'unsupported_cap_send_signature' });
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
} else if (((ts.isCallExpression(expr) && ts.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text)) || (ts.isIdentifier(expr) && wrapperSpecs.has(expr.text)))) {
|
|
422
|
-
const wrapperName = ts.isIdentifier(expr) ? expr.text : ts.isCallExpression(expr) && ts.isIdentifier(expr.expression) ? expr.expression.text : '';
|
|
423
|
-
const wrapperArgs = ts.isIdentifier(expr) ? node.arguments : ts.isCallExpression(expr) ? expr.arguments : node.arguments;
|
|
424
|
-
const spec = wrapperSpecs.get(wrapperName);
|
|
425
|
-
const clientArg = spec?.clientIndex === undefined ? undefined : wrapperArgs[spec.clientIndex];
|
|
426
|
-
const pathArg = spec ? wrapperArgs[spec.pathIndex] : undefined;
|
|
427
|
-
const methodArg = spec?.methodIndex === undefined ? undefined : wrapperArgs[spec.methodIndex];
|
|
428
|
-
const receiver = clientArg && ts.isIdentifier(clientArg) ? clientArg.text : spec?.clientName;
|
|
429
|
-
const method = stripQuotes(resolveExpression(methodArg, node, 'literal').value ?? spec?.methodLiteral ?? 'POST');
|
|
430
|
-
const pathAnalysis = analyzeOperationPath(pathArg, node, method);
|
|
431
|
-
const operationPathExpr = operationPathExpression(pathAnalysis);
|
|
432
|
-
const normalizedOperationPath = operationPathExpr ? classifyODataPathIntent(operationPathExpr, method).topLevelOperationName : undefined;
|
|
433
|
-
const unresolvedReason = pathUnresolvedReason(pathAnalysis);
|
|
434
|
-
if (spec && receiver && operationPathExpr) {
|
|
435
|
-
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: lineOf(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 });
|
|
436
|
-
} else if (spec && receiver) {
|
|
437
|
-
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: lineOf(source.text, node.getStart(source)), wrapperPathSourceKind: wrapperSourceKind(pathAnalysis.sourceKind), rawPathExpression: pathAnalysis.rawExpression, pathAnalysis, parserWarning: unresolvedReason });
|
|
438
|
-
}
|
|
439
|
-
} else if (ts.isPropertyAccessExpression(expr) && ['emit', 'publish', 'on'].includes(expr.name.text)) {
|
|
440
|
-
const receiver = receiverName(expr.expression);
|
|
441
|
-
const rootReceiver = rootReceiverName(expr.expression);
|
|
442
|
-
if (isSupportedEventReceiver(receiver, rootReceiver, serviceVariables)) {
|
|
443
|
-
const eventName = literalText(node.arguments[0]);
|
|
444
|
-
const effectiveReceiver = rootReceiver ?? receiver;
|
|
445
|
-
const lifecycleHook = effectiveReceiver === 'cds'
|
|
446
|
-
&& CDS_LIFECYCLE_EVENTS.has(eventName ?? '');
|
|
447
|
-
const errorHook = expr.name.text === 'on' && eventName === 'error';
|
|
448
|
-
if (eventName && !lifecycleHook && !errorHook) add(node, { callType: expr.name.text === 'on' ? 'async_subscribe' : 'async_emit', serviceVariableName: effectiveReceiver, eventNameExpr: eventName }, { receiver, rootReceiver, classifier: expr.name.text === 'on' ? 'cap_service_event_subscription' : 'cap_service_event_emit', receiverClassification: 'cap_evidence' });
|
|
449
|
-
}
|
|
450
|
-
} else {
|
|
451
|
-
const external = externalHttpEvidence(node, source);
|
|
452
|
-
if (external) {
|
|
453
|
-
const evidenceTarget = { ...external.externalTarget, method: external.method, parserClassifier: external.classifier, sourceCallShape: external.sourceCallShape };
|
|
454
|
-
const safeTarget = externalHttpTarget({ method: external.method, evidence_json: JSON.stringify({ externalTarget: evidenceTarget }) });
|
|
455
|
-
add(node, { callType: 'external_http', method: external.method, payloadSummary: undefined, confidence: 0.7, unresolvedReason: 'External HTTP destination is outside indexed CAP services', externalTarget: { kind: safeTarget.kind, stableId: safeTarget.toId, label: safeTarget.label, dynamic: safeTarget.dynamic } }, { classifier: external.classifier, externalTarget: safeTarget, sourceCallShape: external.sourceCallShape });
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
ts.forEachChild(node, visit);
|
|
460
|
-
};
|
|
461
|
-
visit(source);
|
|
462
|
-
return calls;
|
|
463
|
-
}
|
|
464
|
-
export function containsSupportedOutboundCall(node: ts.Node): boolean {
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
classifyOutboundCallsInSource,
|
|
21
|
+
type ClassifiedOutboundCall,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
interface LocalServiceOrigin {
|
|
25
|
+
service: string;
|
|
26
|
+
lookup: string;
|
|
27
|
+
chain: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface LocalServiceOperation extends LocalServiceOrigin {
|
|
31
|
+
operation: string;
|
|
32
|
+
classifier: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function containsSupportedOutboundCall(
|
|
36
|
+
node: ts.Node,
|
|
37
|
+
classified?: readonly ClassifiedOutboundCall[],
|
|
38
|
+
): boolean {
|
|
465
39
|
const source = node.getSourceFile();
|
|
466
40
|
const start = node.getFullStart();
|
|
467
41
|
const end = node.getEnd();
|
|
468
|
-
|
|
42
|
+
const calls = classified
|
|
43
|
+
?? classifyOutboundCallsInSource(source, source.fileName);
|
|
44
|
+
return calls.some((call) =>
|
|
45
|
+
call.node.getStart(source) >= start && call.node.getEnd() <= end);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parsedSource(
|
|
49
|
+
filePath: string,
|
|
50
|
+
text: string,
|
|
51
|
+
context?: RepositorySourceContext,
|
|
52
|
+
): ts.SourceFile {
|
|
53
|
+
const snapshot = context?.get(filePath);
|
|
54
|
+
return snapshot?.sourceFile() ?? ts.createSourceFile(
|
|
55
|
+
filePath, text, ts.ScriptTarget.Latest, true,
|
|
56
|
+
filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS,
|
|
57
|
+
);
|
|
469
58
|
}
|
|
59
|
+
|
|
470
60
|
export async function parseOutboundCalls(
|
|
471
61
|
repoPath: string,
|
|
472
62
|
filePath: string,
|
|
473
63
|
context?: RepositorySourceContext,
|
|
64
|
+
classified?: readonly ClassifiedOutboundCall[],
|
|
65
|
+
preparedBindings?: readonly ServiceBindingFact[],
|
|
474
66
|
): Promise<OutboundCallFact[]> {
|
|
475
67
|
const snapshot = context?.get(filePath);
|
|
476
68
|
const text = snapshot?.text
|
|
477
69
|
?? await fs.readFile(path.join(repoPath, filePath), 'utf8');
|
|
478
|
-
const source =
|
|
479
|
-
|
|
480
|
-
filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS,
|
|
481
|
-
);
|
|
482
|
-
const bindingNames = new Set((await parseServiceBindings(
|
|
70
|
+
const source = parsedSource(filePath, text, context);
|
|
71
|
+
const bindings = preparedBindings ?? await parseServiceBindings(
|
|
483
72
|
repoPath, filePath, context,
|
|
484
|
-
)
|
|
73
|
+
);
|
|
74
|
+
const bindingNames = new Set(bindings.map((binding) =>
|
|
75
|
+
binding.variableName));
|
|
485
76
|
const importedWrappers = await parseImportedWrapperCalls(
|
|
486
77
|
repoPath, filePath, source, bindingNames, context,
|
|
487
78
|
);
|
|
488
|
-
|
|
79
|
+
const nativeCalls = classified
|
|
80
|
+
?? classifyOutboundCallsInSource(source, filePath);
|
|
81
|
+
return [
|
|
82
|
+
...nativeCalls.map((call) => call.fact),
|
|
83
|
+
...importedWrappers,
|
|
84
|
+
...parseLocalServiceCalls(text, filePath, source),
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function localServiceCallFact(
|
|
89
|
+
parsed: LocalServiceOperation,
|
|
90
|
+
node: ts.CallExpression,
|
|
91
|
+
text: string,
|
|
92
|
+
filePath: string,
|
|
93
|
+
source: ts.SourceFile,
|
|
94
|
+
): OutboundCallFact {
|
|
95
|
+
const transport = ['send', 'emit', 'publish', 'on']
|
|
96
|
+
.includes(parsed.operation);
|
|
97
|
+
return {
|
|
98
|
+
callType: 'local_service_call',
|
|
99
|
+
operationPathExpr: `/${parsed.operation}`,
|
|
100
|
+
payloadSummary: parsed.service,
|
|
101
|
+
localServiceName: parsed.service,
|
|
102
|
+
localServiceLookup: parsed.lookup,
|
|
103
|
+
aliasChain: parsed.chain,
|
|
104
|
+
sourceFile: normalizePath(filePath),
|
|
105
|
+
sourceLine: lineOf(text, node.getStart(source)),
|
|
106
|
+
callSiteStartOffset: node.getStart(source),
|
|
107
|
+
callSiteEndOffset: node.getEnd(),
|
|
108
|
+
confidence: 0.9,
|
|
109
|
+
unresolvedReason: transport ? 'transport_client_method' : undefined,
|
|
110
|
+
evidence: parserEvidence(source, node, {
|
|
111
|
+
classifier: parsed.classifier,
|
|
112
|
+
parserCallType: parsed.operation === 'send'
|
|
113
|
+
? 'transport_client_method' : parsed.classifier,
|
|
114
|
+
localServiceLookup: parsed.lookup,
|
|
115
|
+
localServiceName: parsed.service,
|
|
116
|
+
operation: parsed.operation,
|
|
117
|
+
aliasChain: parsed.chain,
|
|
118
|
+
}),
|
|
119
|
+
};
|
|
489
120
|
}
|
|
121
|
+
|
|
490
122
|
function parseLocalServiceCalls(
|
|
491
123
|
text: string,
|
|
492
124
|
filePath: string,
|
|
493
|
-
source
|
|
494
|
-
filePath, text, ts.ScriptTarget.Latest, true,
|
|
495
|
-
filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS,
|
|
496
|
-
),
|
|
125
|
+
source: ts.SourceFile,
|
|
497
126
|
): OutboundCallFact[] {
|
|
498
|
-
const aliases = new Map<string,
|
|
127
|
+
const aliases = new Map<string, LocalServiceOrigin>();
|
|
499
128
|
const calls: OutboundCallFact[] = [];
|
|
500
129
|
const visit = (node: ts.Node): void => {
|
|
501
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)
|
|
130
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)
|
|
131
|
+
&& node.initializer) {
|
|
502
132
|
const origin = serviceLookup(node.initializer, aliases);
|
|
503
|
-
if (origin) aliases.set(node.name.text, {
|
|
133
|
+
if (origin) aliases.set(node.name.text, {
|
|
134
|
+
...origin, chain: [...origin.chain, node.name.text],
|
|
135
|
+
});
|
|
504
136
|
}
|
|
505
137
|
if (ts.isCallExpression(node)) {
|
|
506
138
|
const parsed = serviceOperationCall(node, aliases);
|
|
507
|
-
if (parsed && parsed.operation !== 'entities')
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
localServiceName: parsed.service,
|
|
512
|
-
localServiceLookup: parsed.lookup,
|
|
513
|
-
aliasChain: parsed.chain,
|
|
514
|
-
sourceFile: normalizePath(filePath),
|
|
515
|
-
sourceLine: lineOf(text, node.getStart(source)),
|
|
516
|
-
callSiteStartOffset: node.getStart(source),
|
|
517
|
-
callSiteEndOffset: node.getEnd(),
|
|
518
|
-
confidence: 0.9,
|
|
519
|
-
unresolvedReason: ['send', 'emit', 'publish', 'on'].includes(parsed.operation) ? 'transport_client_method' : undefined,
|
|
520
|
-
evidence: parserEvidence(source, node, {
|
|
521
|
-
classifier: parsed.classifier,
|
|
522
|
-
parserCallType: parsed.operation === 'send' ? 'transport_client_method' : parsed.classifier,
|
|
523
|
-
localServiceLookup: parsed.lookup,
|
|
524
|
-
localServiceName: parsed.service,
|
|
525
|
-
operation: parsed.operation,
|
|
526
|
-
aliasChain: parsed.chain,
|
|
527
|
-
}),
|
|
528
|
-
});
|
|
139
|
+
if (parsed && parsed.operation !== 'entities')
|
|
140
|
+
calls.push(localServiceCallFact(
|
|
141
|
+
parsed, node, text, filePath, source,
|
|
142
|
+
));
|
|
529
143
|
}
|
|
530
144
|
ts.forEachChild(node, visit);
|
|
531
145
|
};
|
|
532
146
|
visit(source);
|
|
533
147
|
return calls;
|
|
534
148
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
149
|
+
|
|
150
|
+
function serviceLookup(
|
|
151
|
+
expression: ts.Expression,
|
|
152
|
+
aliases: Map<string, LocalServiceOrigin>,
|
|
153
|
+
): LocalServiceOrigin | undefined {
|
|
154
|
+
if (ts.isIdentifier(expression)) return aliases.get(expression.text);
|
|
155
|
+
if (ts.isPropertyAccessExpression(expression)
|
|
156
|
+
&& expression.expression.getText() === 'cds.services') return {
|
|
157
|
+
service: expression.name.text,
|
|
158
|
+
lookup: expression.getText(),
|
|
159
|
+
chain: [expression.getText()],
|
|
160
|
+
};
|
|
161
|
+
if (!ts.isElementAccessExpression(expression)
|
|
162
|
+
|| expression.expression.getText() !== 'cds.services'
|
|
163
|
+
|| !ts.isStringLiteral(expression.argumentExpression)) return undefined;
|
|
164
|
+
return {
|
|
165
|
+
service: expression.argumentExpression.text,
|
|
166
|
+
lookup: expression.getText(),
|
|
167
|
+
chain: [expression.getText()],
|
|
168
|
+
};
|
|
540
169
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
170
|
+
|
|
171
|
+
function positionalSendOperation(
|
|
172
|
+
node: ts.CallExpression,
|
|
173
|
+
): { operation: string; classifier: string } | undefined {
|
|
174
|
+
const first = literalText(node.arguments[0]);
|
|
175
|
+
const second = literalText(node.arguments[1]);
|
|
176
|
+
const method = first?.toUpperCase();
|
|
177
|
+
if (method && ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']
|
|
178
|
+
.includes(method) && second) return {
|
|
179
|
+
operation: second.replace(/^\//, ''),
|
|
180
|
+
classifier: 'cap_service_send_method_path',
|
|
181
|
+
};
|
|
182
|
+
return first ? {
|
|
183
|
+
operation: first.replace(/^\//, ''),
|
|
184
|
+
classifier: 'cap_service_send_local_dispatch',
|
|
185
|
+
} : undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function serviceOperationCall(
|
|
189
|
+
node: ts.CallExpression,
|
|
190
|
+
aliases: Map<string, LocalServiceOrigin>,
|
|
191
|
+
): LocalServiceOperation | undefined {
|
|
192
|
+
const expression = node.expression;
|
|
193
|
+
if (!ts.isPropertyAccessExpression(expression)) return undefined;
|
|
194
|
+
const origin = serviceLookup(expression.expression, aliases);
|
|
545
195
|
if (!origin) return undefined;
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
196
|
+
const send = expression.name.text === 'send'
|
|
197
|
+
? positionalSendOperation(node) : undefined;
|
|
198
|
+
if (send) return { ...origin, ...send };
|
|
199
|
+
return {
|
|
200
|
+
...origin,
|
|
201
|
+
operation: expression.name.text,
|
|
202
|
+
classifier: 'local_cap_service_call',
|
|
203
|
+
};
|
|
554
204
|
}
|