@saptools/service-flow 0.1.39 → 0.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +4 -0
- package/TECHNICAL-NOTE.md +4 -0
- package/dist/{chunk-SAZ5OK7R.js → chunk-7L4QKKGN.js} +189 -86
- package/dist/chunk-7L4QKKGN.js.map +1 -0
- package/dist/cli.js +79 -9
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/db/migrations.ts +14 -1
- package/src/db/repositories.ts +9 -2
- package/src/db/schema.ts +4 -2
- package/src/indexer/cds-extension-resolver.ts +65 -0
- package/src/indexer/workspace-indexer.ts +2 -0
- package/src/parsers/cds-parser.ts +43 -16
- package/src/parsers/outbound-call-parser.ts +142 -62
- package/src/types.ts +10 -0
- package/dist/chunk-SAZ5OK7R.js.map +0 -1
|
@@ -23,11 +23,12 @@ function expressionName(expr: ts.Expression): string {
|
|
|
23
23
|
}
|
|
24
24
|
function variableInitializers(source: ts.SourceFile): Map<string, ts.Expression> {
|
|
25
25
|
const initializers = new Map<string, ts.Expression>();
|
|
26
|
-
const
|
|
27
|
-
if (ts.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
for (const statement of source.statements) {
|
|
27
|
+
if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
|
|
28
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
29
|
+
if (ts.isIdentifier(declaration.name) && declaration.initializer) initializers.set(declaration.name.text, declaration.initializer);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
31
32
|
return initializers;
|
|
32
33
|
}
|
|
33
34
|
function queryEntityFromAst(expr: ts.Expression, initializers = new Map<string, ts.Expression>()): string | undefined {
|
|
@@ -89,50 +90,119 @@ function nameOfProperty(name: ts.PropertyName): string | undefined {
|
|
|
89
90
|
return undefined;
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
type ExpressionStatus = 'static' | 'dynamic' | 'ambiguous' | 'unknown';
|
|
94
|
+
type ExpressionSourceKind = 'string_literal' | 'no_substitution_template' | 'template_with_substitutions' | 'const_alias' | 'conditional_candidates' | 'dynamic_expression';
|
|
95
|
+
interface ExpressionResolution { status: ExpressionStatus; sourceKind: ExpressionSourceKind; value?: string; rawExpression?: string; placeholderKeys: string[]; evidence: string[]; constName?: string }
|
|
96
|
+
interface BindingResolution { declaration?: ts.VariableDeclaration | ts.ParameterDeclaration; initializer?: ts.Expression; immutable: boolean; evidence: string[] }
|
|
97
|
+
const maxAliasDepth = 5;
|
|
98
|
+
function safeRaw(expr: ts.Expression): string | undefined {
|
|
99
|
+
if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr) || ts.isIdentifier(expr) || ts.isTemplateExpression(expr)) return expr.getText(expr.getSourceFile());
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
function placeholders(expr: ts.TemplateExpression): string[] {
|
|
103
|
+
return expr.templateSpans.map((span) => span.expression.getText(expr.getSourceFile()));
|
|
104
|
+
}
|
|
105
|
+
function isFunctionLikeScope(node: ts.Node): boolean {
|
|
106
|
+
return ts.isFunctionLike(node) || ts.isSourceFile(node);
|
|
107
|
+
}
|
|
108
|
+
function nodeContains(parent: ts.Node, child: ts.Node): boolean {
|
|
109
|
+
const source = child.getSourceFile();
|
|
110
|
+
return child.getStart(source) >= parent.getStart(source) && child.getEnd() <= parent.getEnd();
|
|
111
|
+
}
|
|
112
|
+
function declarationScope(node: ts.VariableDeclaration | ts.ParameterDeclaration): ts.Node {
|
|
113
|
+
if (ts.isParameter(node)) return node.parent;
|
|
114
|
+
const list = node.parent;
|
|
115
|
+
const blockScoped = (list.flags & (ts.NodeFlags.Const | ts.NodeFlags.Let)) !== 0;
|
|
116
|
+
let current: ts.Node = list.parent;
|
|
117
|
+
if (!blockScoped) {
|
|
118
|
+
while (current.parent && !isFunctionLikeScope(current)) current = current.parent;
|
|
119
|
+
return current;
|
|
120
|
+
}
|
|
121
|
+
while (current.parent && !ts.isBlock(current) && !ts.isSourceFile(current) && !ts.isModuleBlock(current) && !ts.isCaseBlock(current) && !isFunctionLikeScope(current)) current = current.parent;
|
|
122
|
+
return current;
|
|
123
|
+
}
|
|
124
|
+
function isAccessibleDeclaration(declaration: ts.VariableDeclaration | ts.ParameterDeclaration, use: ts.Node): boolean {
|
|
125
|
+
const source = use.getSourceFile();
|
|
126
|
+
if (declaration.name.getStart(source) >= use.getStart(source)) return false;
|
|
127
|
+
const scope = declarationScope(declaration);
|
|
128
|
+
return ts.isSourceFile(scope) || nodeContains(scope, use);
|
|
129
|
+
}
|
|
130
|
+
function resolveBinding(identifier: ts.Identifier, use: ts.Node): BindingResolution {
|
|
131
|
+
const source = use.getSourceFile();
|
|
132
|
+
let best: ts.VariableDeclaration | ts.ParameterDeclaration | undefined;
|
|
133
|
+
const visit = (node: ts.Node): void => {
|
|
134
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
|
|
135
|
+
if (ts.isParameter(node) && ts.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
|
|
136
|
+
ts.forEachChild(node, visit);
|
|
137
|
+
};
|
|
138
|
+
visit(source);
|
|
139
|
+
if (!best) return { immutable: false, evidence: ['binding_not_found'] };
|
|
140
|
+
const immutable = ts.isVariableDeclaration(best) && (best.parent.flags & ts.NodeFlags.Const) !== 0;
|
|
141
|
+
return { declaration: best, initializer: ts.isVariableDeclaration(best) ? best.initializer : undefined, immutable, evidence: [immutable ? 'lexical_const_binding_before_use' : 'lexical_mutable_or_parameter_binding'] };
|
|
142
|
+
}
|
|
143
|
+
function resolveExpression(expr: ts.Expression | undefined, use: ts.Node, policy: 'operation_path' | 'external' | 'literal', depth = 0, seen = new Set<ts.Node>()): ExpressionResolution {
|
|
144
|
+
if (!expr) return { status: 'unknown', sourceKind: 'dynamic_expression', placeholderKeys: [], evidence: ['expression_missing'] };
|
|
145
|
+
if (ts.isStringLiteral(expr)) return { status: 'static', sourceKind: 'string_literal', value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['string_literal'] };
|
|
146
|
+
if (ts.isNoSubstitutionTemplateLiteral(expr)) return { status: 'static', sourceKind: 'no_substitution_template', value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['no_substitution_template'] };
|
|
147
|
+
if (ts.isTemplateExpression(expr)) {
|
|
148
|
+
const keys = placeholders(expr);
|
|
149
|
+
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'] };
|
|
150
|
+
return { status: 'dynamic', sourceKind: 'template_with_substitutions', placeholderKeys: keys, evidence: ['template_substitutions_not_static_external_target'] };
|
|
151
|
+
}
|
|
152
|
+
if (ts.isIdentifier(expr)) {
|
|
153
|
+
if (depth >= maxAliasDepth) return { status: 'unknown', sourceKind: 'const_alias', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['alias_depth_exceeded'], constName: expr.text };
|
|
154
|
+
const binding = resolveBinding(expr, use);
|
|
155
|
+
if (!binding.declaration || !binding.initializer || !binding.immutable) return { status: 'dynamic', sourceKind: 'dynamic_expression', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: binding.evidence, constName: expr.text };
|
|
156
|
+
if (seen.has(binding.declaration)) return { status: 'unknown', sourceKind: 'const_alias', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['alias_cycle_detected'], constName: expr.text };
|
|
157
|
+
seen.add(binding.declaration);
|
|
158
|
+
const resolved = resolveExpression(binding.initializer, binding.declaration, policy, depth + 1, seen);
|
|
159
|
+
return { ...resolved, sourceKind: 'const_alias', rawExpression: safeRaw(expr), constName: expr.text, evidence: [...binding.evidence, ...resolved.evidence] };
|
|
160
|
+
}
|
|
161
|
+
return { status: 'dynamic', sourceKind: 'dynamic_expression', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: [`unsupported_${ts.SyntaxKind[expr.kind] ?? 'expression'}`] };
|
|
162
|
+
}
|
|
92
163
|
function staticExpressionText(expr: ts.Expression | undefined, initializers: Map<string, ts.Expression>): string | undefined {
|
|
93
164
|
if (!expr) return undefined;
|
|
94
165
|
if (isStringLike(expr)) return expr.text;
|
|
95
|
-
if (ts.isTemplateExpression(expr)) return stripQuotes(expr.getText(expr.getSourceFile()));
|
|
96
166
|
if (ts.isIdentifier(expr) && initializers.has(expr.text)) return staticExpressionText(initializers.get(expr.text), initializers);
|
|
97
167
|
return undefined;
|
|
98
168
|
}
|
|
99
|
-
interface StaticPathResolution { text?: string; sourceKind: 'literal' | 'template' | 'const' | 'dynamic'; rawExpression?: string; constName?: string }
|
|
100
|
-
function staticPathExpression(expr: ts.Expression | undefined,
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
if (ts.isTemplateExpression(expr)) return { text: stripQuotes(rawExpression), sourceKind: 'template', rawExpression };
|
|
105
|
-
if (ts.isIdentifier(expr) && initializers.has(expr.text)) {
|
|
106
|
-
const resolved = staticPathExpression(initializers.get(expr.text), initializers);
|
|
107
|
-
return resolved.text ? { ...resolved, sourceKind: 'const', rawExpression, constName: expr.text } : { sourceKind: 'dynamic', rawExpression };
|
|
108
|
-
}
|
|
109
|
-
return { sourceKind: 'dynamic', rawExpression };
|
|
169
|
+
interface StaticPathResolution { text?: string; sourceKind: 'literal' | 'template' | 'const' | 'dynamic'; rawExpression?: string; constName?: string; resolution: ExpressionResolution }
|
|
170
|
+
function staticPathExpression(expr: ts.Expression | undefined, use: ts.Node): StaticPathResolution {
|
|
171
|
+
const resolution = resolveExpression(expr, use, 'operation_path');
|
|
172
|
+
const sourceKind = resolution.sourceKind === 'const_alias' ? 'const' : resolution.sourceKind === 'template_with_substitutions' || resolution.sourceKind === 'no_substitution_template' ? 'template' : resolution.status === 'static' ? 'literal' : 'dynamic';
|
|
173
|
+
return { text: resolution.value, sourceKind, rawExpression: resolution.rawExpression, constName: resolution.constName, resolution };
|
|
110
174
|
}
|
|
111
175
|
function operationPathFromStatic(text: string | undefined): string | undefined {
|
|
112
176
|
return text ? `/${stripQuotes(text).replace(/^\//, '')}` : undefined;
|
|
113
177
|
}
|
|
114
|
-
interface PathCandidateEvidence { candidatePaths: string[]; normalizedCandidateOperations: string[]; candidateSourceKind: string; candidateIdentifier: string; hasDynamicAssignments: boolean }
|
|
115
|
-
function staticPathCandidates(
|
|
178
|
+
interface PathCandidateEvidence { candidatePaths: string[]; normalizedCandidateOperations: string[]; candidateSourceKind: string; candidateIdentifier: string; hasDynamicAssignments: boolean; conservativeReason?: string }
|
|
179
|
+
function staticPathCandidates(identifier: ts.Identifier, call: ts.Node, method: string): PathCandidateEvidence | undefined {
|
|
180
|
+
const binding = resolveBinding(identifier, call);
|
|
181
|
+
const declaration = binding.declaration;
|
|
182
|
+
if (!declaration) return undefined;
|
|
183
|
+
const source = call.getSourceFile();
|
|
116
184
|
const paths: string[] = [];
|
|
117
185
|
let hasDynamicAssignments = false;
|
|
186
|
+
const addExpr = (expr: ts.Expression | undefined, origin: ts.Node): void => {
|
|
187
|
+
const resolved = staticPathExpression(expr, origin);
|
|
188
|
+
if (resolved.text) paths.push(operationPathFromStatic(resolved.text) ?? resolved.text);
|
|
189
|
+
else hasDynamicAssignments = true;
|
|
190
|
+
};
|
|
191
|
+
if (ts.isVariableDeclaration(declaration)) addExpr(declaration.initializer, declaration);
|
|
118
192
|
const visit = (node: ts.Node): void => {
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(node.left) && node.left.text === identifier) {
|
|
125
|
-
const resolved = staticPathExpression(node.right, initializers);
|
|
126
|
-
if (resolved.text) paths.push(operationPathFromStatic(resolved.text) ?? resolved.text);
|
|
127
|
-
else hasDynamicAssignments = true;
|
|
193
|
+
if (node !== source && isFunctionLikeScope(node) && !nodeContains(node, call)) return;
|
|
194
|
+
if (node.getStart(source) >= call.getStart(source)) return;
|
|
195
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(node.left)) {
|
|
196
|
+
const target = resolveBinding(node.left, node);
|
|
197
|
+
if (target.declaration === declaration) addExpr(node.right, node);
|
|
128
198
|
}
|
|
129
199
|
ts.forEachChild(node, visit);
|
|
130
200
|
};
|
|
131
201
|
visit(source);
|
|
132
202
|
const candidatePaths = [...new Set(paths)];
|
|
133
203
|
if (candidatePaths.length === 0) return undefined;
|
|
134
|
-
const normalizedCandidateOperations = [...new Set(candidatePaths.map((candidate) => classifyODataPathIntent(candidate,
|
|
135
|
-
return { candidatePaths, normalizedCandidateOperations, candidateSourceKind: 'static candidates observed', candidateIdentifier: identifier, hasDynamicAssignments };
|
|
204
|
+
const normalizedCandidateOperations = [...new Set(candidatePaths.map((candidate) => classifyODataPathIntent(candidate, method).topLevelOperationName).filter((value): value is string => Boolean(value)))];
|
|
205
|
+
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' : undefined };
|
|
136
206
|
}
|
|
137
207
|
function destinationExpressionShape(expr: ts.Expression | undefined): string | undefined {
|
|
138
208
|
if (!expr) return undefined;
|
|
@@ -159,57 +229,59 @@ function propertyInitializer(object: ts.ObjectLiteralExpression, key: string): t
|
|
|
159
229
|
}
|
|
160
230
|
return undefined;
|
|
161
231
|
}
|
|
162
|
-
function httpMethodFromObject(object: ts.ObjectLiteralExpression,
|
|
163
|
-
const text =
|
|
232
|
+
function httpMethodFromObject(object: ts.ObjectLiteralExpression, use: ts.Node): string | undefined {
|
|
233
|
+
const text = resolveExpression(propertyInitializer(object, 'method'), use, 'literal').value;
|
|
164
234
|
return text ? stripQuotes(text).toUpperCase() : undefined;
|
|
165
235
|
}
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (
|
|
236
|
+
function hasTemplatePlaceholder(value: string): boolean { return /\$\{|%7B|%7D/i.test(value); }
|
|
237
|
+
function urlTargetFromExpression(expr: ts.Expression | undefined, use: ts.Node): Record<string, unknown> {
|
|
238
|
+
const resolved = resolveExpression(expr, use, 'external');
|
|
239
|
+
if (resolved.status === 'static' && resolved.value && !hasTemplatePlaceholder(resolved.value)) return { kind: 'static_url', expression: resolved.value, dynamic: false, sourceKind: resolved.sourceKind };
|
|
240
|
+
if (expr) return { kind: 'url_expression', dynamic: true, expression: `${resolved.sourceKind}:${resolved.placeholderKeys.join('|')}`, expressionShape: resolved.sourceKind, placeholderKeys: resolved.placeholderKeys };
|
|
170
241
|
return { kind: 'unknown', dynamic: false };
|
|
171
242
|
}
|
|
172
|
-
function destinationTargetFromExpression(expr: ts.Expression | undefined,
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
243
|
+
function destinationTargetFromExpression(expr: ts.Expression | undefined, use: ts.Node): Record<string, unknown> | undefined {
|
|
244
|
+
const resolved = resolveExpression(expr, use, 'external');
|
|
245
|
+
const text = resolved.value;
|
|
246
|
+
if (resolved.status === 'static' && text && !hasTemplatePlaceholder(text)) return { kind: 'destination', expression: text, dynamic: false, sourceKind: resolved.sourceKind };
|
|
247
|
+
const candidates = staticConditionalCandidates(expr, new Map<string, ts.Expression>());
|
|
176
248
|
if (candidates) return { kind: 'destination', dynamic: true, expressionShape: 'conditional', candidateLiterals: candidates };
|
|
177
249
|
const shape = destinationExpressionShape(expr);
|
|
178
250
|
if (shape) return { kind: 'destination', dynamic: true, expressionShape: shape };
|
|
179
251
|
return undefined;
|
|
180
252
|
}
|
|
181
|
-
function externalHttpEvidence(node: ts.CallExpression, source: ts.SourceFile
|
|
253
|
+
function externalHttpEvidence(node: ts.CallExpression, source: ts.SourceFile): { method?: string; externalTarget: Record<string, unknown>; classifier: string; sourceCallShape: string } | undefined {
|
|
182
254
|
const expr = node.expression;
|
|
183
255
|
const exprText = expr.getText(source);
|
|
184
256
|
if (exprText === 'useOrFetchDestination') {
|
|
185
257
|
const objectArg = node.arguments[0];
|
|
186
258
|
if (objectArg && ts.isObjectLiteralExpression(objectArg)) {
|
|
187
|
-
const destination = destinationTargetFromExpression(propertyInitializer(objectArg, 'destinationName'),
|
|
259
|
+
const destination = destinationTargetFromExpression(propertyInitializer(objectArg, 'destinationName'), node);
|
|
188
260
|
return { externalTarget: destination ?? { kind: 'unknown', dynamic: false }, classifier: 'sap_destination_lookup', sourceCallShape: 'useOrFetchDestination' };
|
|
189
261
|
}
|
|
190
262
|
}
|
|
191
263
|
if (exprText === 'executeHttpRequest') {
|
|
192
|
-
const destination = destinationTargetFromExpression(node.arguments[0],
|
|
264
|
+
const destination = destinationTargetFromExpression(node.arguments[0], node);
|
|
193
265
|
const config = node.arguments[1];
|
|
194
|
-
const method = config && ts.isObjectLiteralExpression(config) ? httpMethodFromObject(config,
|
|
195
|
-
const url = config && ts.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, 'url'),
|
|
266
|
+
const method = config && ts.isObjectLiteralExpression(config) ? httpMethodFromObject(config, node) : undefined;
|
|
267
|
+
const url = config && ts.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, 'url'), node) : { kind: 'unknown', dynamic: false };
|
|
196
268
|
return { method, externalTarget: destination ? { ...url, destination } : url, classifier: 'sap_execute_http_request', sourceCallShape: 'executeHttpRequest' };
|
|
197
269
|
}
|
|
198
270
|
if (exprText === 'axios') {
|
|
199
271
|
const config = node.arguments[0];
|
|
200
272
|
if (config && ts.isObjectLiteralExpression(config)) {
|
|
201
|
-
const method = httpMethodFromObject(config,
|
|
202
|
-
return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, 'url'),
|
|
273
|
+
const method = httpMethodFromObject(config, node);
|
|
274
|
+
return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, 'url'), node), classifier: 'axios_config_call', sourceCallShape: 'axios(config)' };
|
|
203
275
|
}
|
|
204
276
|
return { externalTarget: { kind: 'unknown', dynamic: false }, classifier: 'axios_unknown_call', sourceCallShape: 'axios(...)' };
|
|
205
277
|
}
|
|
206
278
|
if (exprText === 'fetch') {
|
|
207
279
|
const init = node.arguments[1];
|
|
208
|
-
const method = init && ts.isObjectLiteralExpression(init) ? httpMethodFromObject(init,
|
|
209
|
-
return { method, externalTarget: urlTargetFromExpression(node.arguments[0],
|
|
280
|
+
const method = init && ts.isObjectLiteralExpression(init) ? httpMethodFromObject(init, node) : undefined;
|
|
281
|
+
return { method, externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: 'fetch_call', sourceCallShape: 'fetch' };
|
|
210
282
|
}
|
|
211
283
|
if (ts.isPropertyAccessExpression(expr) && ['get','post','put','patch','delete','head'].includes(expr.name.text) && expr.expression.getText(source) === 'axios') {
|
|
212
|
-
return { method: expr.name.text.toUpperCase(), externalTarget: urlTargetFromExpression(node.arguments[0],
|
|
284
|
+
return { method: expr.name.text.toUpperCase(), externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: 'axios_member_call', sourceCallShape: `axios.${expr.name.text}` };
|
|
213
285
|
}
|
|
214
286
|
return undefined;
|
|
215
287
|
}
|
|
@@ -274,7 +346,7 @@ function collectWrapperSpecs(source: ts.SourceFile): Map<string, WrapperSpec> {
|
|
|
274
346
|
const methodProp = propertyInitializer(objectArg, 'method');
|
|
275
347
|
const pathName = pathProp && ts.isIdentifier(pathProp) ? pathProp.text : undefined;
|
|
276
348
|
const methodName = methodProp && ts.isIdentifier(methodProp) ? methodProp.text : undefined;
|
|
277
|
-
const methodLiteral =
|
|
349
|
+
const methodLiteral = resolveExpression(methodProp, node, 'literal').value;
|
|
278
350
|
if (pathName) sends.push({ client: node.expression.expression.text, path: pathName, method: methodName, methodLiteral, start: node.getStart(source), end: node.getEnd() });
|
|
279
351
|
}
|
|
280
352
|
}
|
|
@@ -324,12 +396,12 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
|
|
|
324
396
|
if (objectArg && ts.isObjectLiteralExpression(objectArg)) {
|
|
325
397
|
const receiver = receiverName(expr.expression);
|
|
326
398
|
const query = objectPropertyText(objectArg, 'query');
|
|
327
|
-
const method = stripQuotes(objectPropertyText(objectArg, 'method') ?? 'POST');
|
|
399
|
+
const method = stripQuotes(resolveExpression(propertyInitializer(objectArg, 'method'), node, 'literal').value ?? objectPropertyText(objectArg, 'method') ?? 'POST');
|
|
328
400
|
const pathExpr = propertyInitializer(objectArg, 'path') ?? propertyInitializer(objectArg, 'event');
|
|
329
|
-
const resolvedPath = staticPathExpression(pathExpr,
|
|
401
|
+
const resolvedPath = staticPathExpression(pathExpr, node);
|
|
330
402
|
const op = pathExpr ? resolvedPath.text ?? pathExpr.getText(source) : undefined;
|
|
331
403
|
const shorthandPath = objectPropertyIsShorthand(objectArg, 'path');
|
|
332
|
-
const candidateEvidence = pathExpr && ts.isIdentifier(pathExpr) && !resolvedPath.text ? staticPathCandidates(
|
|
404
|
+
const candidateEvidence = pathExpr && ts.isIdentifier(pathExpr) && !resolvedPath.text ? staticPathCandidates(pathExpr, node, method) : undefined;
|
|
333
405
|
const candidateOperationPath = candidateEvidence && !candidateEvidence.hasDynamicAssignments && candidateEvidence.normalizedCandidateOperations.length === 1 && candidateEvidence.candidatePaths.length > 0 ? candidateEvidence.candidatePaths[0] : undefined;
|
|
334
406
|
const operationPathExpr = operationPathFromStatic(resolvedPath.text) ?? candidateOperationPath;
|
|
335
407
|
const intent = classifyODataPathIntent(operationPathExpr, method);
|
|
@@ -346,8 +418,8 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
|
|
|
346
418
|
const pathArg = spec ? wrapperArgs[spec.pathIndex] : undefined;
|
|
347
419
|
const methodArg = spec?.methodIndex === undefined ? undefined : wrapperArgs[spec.methodIndex];
|
|
348
420
|
const receiver = clientArg && ts.isIdentifier(clientArg) ? clientArg.text : spec?.clientName;
|
|
349
|
-
const method = stripQuotes(
|
|
350
|
-
const resolvedPath = staticPathExpression(pathArg,
|
|
421
|
+
const method = stripQuotes(resolveExpression(methodArg, node, 'literal').value ?? spec?.methodLiteral ?? 'POST');
|
|
422
|
+
const resolvedPath = staticPathExpression(pathArg, node);
|
|
351
423
|
const operationPathExpr = operationPathFromStatic(resolvedPath.text);
|
|
352
424
|
const normalizedOperationPath = operationPathExpr ? classifyODataPathIntent(operationPathExpr, method).topLevelOperationName : undefined;
|
|
353
425
|
if (spec && receiver && operationPathExpr) {
|
|
@@ -363,7 +435,7 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
|
|
|
363
435
|
if (eventName) add(node, { callType: expr.name.text === 'on' ? 'async_subscribe' : 'async_emit', serviceVariableName: rootReceiver ?? receiver, eventNameExpr: eventName }, { receiver, rootReceiver, classifier: expr.name.text === 'on' ? 'cap_service_event_subscription' : 'cap_service_event_emit', receiverClassification: 'cap_evidence' });
|
|
364
436
|
}
|
|
365
437
|
} else {
|
|
366
|
-
const external = externalHttpEvidence(node, source
|
|
438
|
+
const external = externalHttpEvidence(node, source);
|
|
367
439
|
if (external) {
|
|
368
440
|
const evidenceTarget = { ...external.externalTarget, method: external.method, parserClassifier: external.classifier, sourceCallShape: external.sourceCallShape };
|
|
369
441
|
const safeTarget = externalHttpTarget({ method: external.method, evidence_json: JSON.stringify({ externalTarget: evidenceTarget }) });
|
|
@@ -400,7 +472,7 @@ function parseLocalServiceCalls(text: string, filePath: string): OutboundCallFac
|
|
|
400
472
|
if (origin) aliases.set(node.name.text, { ...origin, chain: [...origin.chain, node.name.text] });
|
|
401
473
|
}
|
|
402
474
|
if (ts.isCallExpression(node)) {
|
|
403
|
-
const parsed = serviceOperationCall(node
|
|
475
|
+
const parsed = serviceOperationCall(node, aliases);
|
|
404
476
|
if (parsed && parsed.operation !== 'entities') calls.push({
|
|
405
477
|
callType: 'local_service_call',
|
|
406
478
|
operationPathExpr: `/${parsed.operation}`,
|
|
@@ -413,7 +485,8 @@ function parseLocalServiceCalls(text: string, filePath: string): OutboundCallFac
|
|
|
413
485
|
confidence: 0.9,
|
|
414
486
|
unresolvedReason: ['send', 'emit', 'publish', 'on'].includes(parsed.operation) ? 'transport_client_method' : undefined,
|
|
415
487
|
evidence: parserEvidence(source, node, {
|
|
416
|
-
classifier:
|
|
488
|
+
classifier: parsed.classifier,
|
|
489
|
+
parserCallType: parsed.operation === 'send' ? 'transport_client_method' : parsed.classifier,
|
|
417
490
|
localServiceLookup: parsed.lookup,
|
|
418
491
|
localServiceName: parsed.service,
|
|
419
492
|
operation: parsed.operation,
|
|
@@ -432,10 +505,17 @@ function serviceLookup(expr: ts.Expression, aliases: Map<string, { service: stri
|
|
|
432
505
|
if (ts.isElementAccessExpression(expr) && expr.expression.getText() === 'cds.services' && ts.isStringLiteral(expr.argumentExpression)) return { service: expr.argumentExpression.text, lookup: expr.getText(), chain: [expr.getText()] };
|
|
433
506
|
return undefined;
|
|
434
507
|
}
|
|
435
|
-
function serviceOperationCall(
|
|
508
|
+
function serviceOperationCall(node: ts.CallExpression, aliases: Map<string, { service: string; lookup: string; chain: string[] }>): { service: string; lookup: string; chain: string[]; operation: string; classifier: string } | undefined {
|
|
509
|
+
const expr = node.expression;
|
|
436
510
|
if (!ts.isPropertyAccessExpression(expr)) return undefined;
|
|
437
|
-
const operation = expr.name.text;
|
|
438
511
|
const origin = serviceLookup(expr.expression, aliases);
|
|
439
512
|
if (!origin) return undefined;
|
|
440
|
-
|
|
513
|
+
if (expr.name.text === 'send') {
|
|
514
|
+
const first = literalText(node.arguments[0]);
|
|
515
|
+
const second = literalText(node.arguments[1]);
|
|
516
|
+
const method = first?.toUpperCase();
|
|
517
|
+
if (method && ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'].includes(method) && second) return { ...origin, operation: second.replace(/^\//, ''), classifier: 'cap_service_send_method_path' };
|
|
518
|
+
if (first) return { ...origin, operation: first.replace(/^\//, ''), classifier: 'cap_service_send_local_dispatch' };
|
|
519
|
+
}
|
|
520
|
+
return { ...origin, operation: expr.name.text, classifier: 'local_cap_service_call' };
|
|
441
521
|
}
|
package/src/types.ts
CHANGED
|
@@ -70,6 +70,14 @@ export interface CdsServiceFact {
|
|
|
70
70
|
sourceFile: string;
|
|
71
71
|
sourceLine: number;
|
|
72
72
|
operations: CdsOperationFact[];
|
|
73
|
+
extension?: CdsExtensionFact;
|
|
74
|
+
}
|
|
75
|
+
export interface CdsExtensionFact {
|
|
76
|
+
localReference: string;
|
|
77
|
+
importedSymbol?: string;
|
|
78
|
+
localAlias?: string;
|
|
79
|
+
moduleSpecifier?: string;
|
|
80
|
+
importKind?: 'relative' | 'package' | 'none';
|
|
73
81
|
}
|
|
74
82
|
export interface CdsOperationFact {
|
|
75
83
|
operationType: 'action' | 'function' | 'event';
|
|
@@ -79,6 +87,8 @@ export interface CdsOperationFact {
|
|
|
79
87
|
returnType?: string;
|
|
80
88
|
sourceFile: string;
|
|
81
89
|
sourceLine: number;
|
|
90
|
+
provenance?: 'direct' | 'inherited';
|
|
91
|
+
baseOperationId?: number;
|
|
82
92
|
}
|
|
83
93
|
export interface HandlerClassFact {
|
|
84
94
|
className: string;
|