@saptools/service-flow 0.1.39 → 0.1.40

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/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  stripQuotes,
17
17
  substituteVariables,
18
18
  trace
19
- } from "./chunk-SAZ5OK7R.js";
19
+ } from "./chunk-774PPGGK.js";
20
20
 
21
21
  // src/parsers/generated-constants-parser.ts
22
22
  import fs from "fs/promises";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saptools/service-flow",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "description": "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -73,17 +73,21 @@ function collectAnnotations(text: string, index: number): { end: number; raw: st
73
73
  }
74
74
 
75
75
  function pathAnnotation(raw: string): string | undefined {
76
- return /path\s*:\s*['"]([^'"]+)['"]/s.exec(raw)?.[1];
76
+ return /path\s*:\s*(['"])(.*?)\1/s.exec(raw)?.[2];
77
77
  }
78
78
 
79
- function matchingBrace(text: string, open: number): number {
79
+ function matchingBrace(maskedText: string, open: number): number {
80
80
  let depth = 0;
81
- for (let i = open; i < text.length; i += 1) {
82
- if (text[i] === '{') depth += 1;
83
- if (text[i] === '}') depth -= 1;
81
+ for (let i = open; i < maskedText.length; i += 1) {
82
+ if (maskedText[i] === '{') depth += 1;
83
+ if (maskedText[i] === '}') depth -= 1;
84
84
  if (depth === 0) return i;
85
85
  }
86
- return text.length - 1;
86
+ return maskedText.length - 1;
87
+ }
88
+ function annotationRawAt(original: string, masked: string, index: number): { end: number; raw: string } {
89
+ const collected = collectAnnotations(masked, index);
90
+ return { end: collected.end, raw: original.slice(index, collected.end) };
87
91
  }
88
92
 
89
93
  function operationsFromBody(text: string, maskedBody: string, bodyOffset: number, filePath: string): CdsOperationFact[] {
@@ -105,16 +109,18 @@ export async function parseCdsFile(repoPath: string, filePath: string): Promise<
105
109
  const namespace = /namespace\s+([\w.]+)\s*;/.exec(masked)?.[1];
106
110
  const services: CdsServiceFact[] = [];
107
111
  const pendingAnnotations: Array<{ end: number; raw: string }> = [];
108
- for (const a of masked.matchAll(/@\s*\(/g)) {
109
- const annotation = collectAnnotations(masked, a.index ?? 0);
110
- pendingAnnotations.push(annotation);
111
- }
112
- const serviceRegex = /\b(extend\s+)?service\s+([\w.]+)\b/g;
112
+ for (const a of masked.matchAll(/@\s*\(/g)) pendingAnnotations.push(annotationRawAt(text, masked, a.index ?? 0));
113
+ const serviceRegex = /\b(?:(extend)\s+)?(?:(service)\s+)?([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\b/g;
113
114
  let match: RegExpExecArray | null;
114
115
  while ((match = serviceRegex.exec(masked))) {
115
- const afterName = collectAnnotations(masked, serviceRegex.lastIndex);
116
+ const isExtend = match[1] === 'extend';
117
+ const hasServiceKeyword = match[2] === 'service';
118
+ if (!isExtend && !hasServiceKeyword) continue;
119
+ const afterName = annotationRawAt(text, masked, serviceRegex.lastIndex);
116
120
  const open = masked.indexOf('{', afterName.end);
117
121
  if (open === -1) continue;
122
+ const between = masked.slice(afterName.end, open).trim();
123
+ if (between.length > 0) continue;
118
124
  const matchIndex = match.index;
119
125
  const prefix = pendingAnnotations
120
126
  .filter((a) => a.end <= matchIndex && matchIndex - a.end < 8)
@@ -123,7 +129,7 @@ export async function parseCdsFile(repoPath: string, filePath: string): Promise<
123
129
  const annotations = `${prefix}${afterName.raw}`;
124
130
  const end = matchingBrace(masked, open);
125
131
  const body = masked.slice(open + 1, end);
126
- const name = match[2] ?? 'UnknownService';
132
+ const name = match[3] ?? 'UnknownService';
127
133
  const serviceName = name.split('.').pop() ?? name;
128
134
  const servicePath = ensureLeadingSlash(pathAnnotation(annotations) ?? serviceName);
129
135
  services.push({
@@ -131,7 +137,7 @@ export async function parseCdsFile(repoPath: string, filePath: string): Promise<
131
137
  serviceName,
132
138
  qualifiedName: name.includes('.') ? name : namespace ? `${namespace}.${name}` : name,
133
139
  servicePath,
134
- isExtend: Boolean(match[1]),
140
+ isExtend,
135
141
  sourceFile: normalizePath(filePath),
136
142
  sourceLine: lineOf(text, match.index),
137
143
  operations: operationsFromBody(text, body, open + 1, filePath)
@@ -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 visit = (node: ts.Node): void => {
27
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (node.parent.flags & ts.NodeFlags.Const) !== 0) initializers.set(node.name.text, node.initializer);
28
- ts.forEachChild(node, visit);
29
- };
30
- visit(source);
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,114 @@ 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 containingScope(node: ts.Node): ts.Node {
109
+ let current: ts.Node | undefined = node.parent;
110
+ while (current && !isFunctionLikeScope(current)) current = current.parent;
111
+ return current ?? node.getSourceFile();
112
+ }
113
+ function nodeContains(parent: ts.Node, child: ts.Node): boolean {
114
+ return child.getStart(child.getSourceFile()) >= parent.getStart(parent.getSourceFile()) && child.getEnd() <= parent.getEnd();
115
+ }
116
+ function nestedFunctionContains(scope: ts.Node, candidate: ts.Node, use: ts.Node): boolean {
117
+ let current = candidate.parent;
118
+ while (current && current !== scope) {
119
+ if (isFunctionLikeScope(current) && !nodeContains(current, use)) return true;
120
+ current = current.parent;
121
+ }
122
+ return false;
123
+ }
124
+ function resolveBinding(identifier: ts.Identifier, use: ts.Node): BindingResolution {
125
+ const scope = containingScope(use);
126
+ const source = use.getSourceFile();
127
+ let best: ts.VariableDeclaration | ts.ParameterDeclaration | undefined;
128
+ const visit = (node: ts.Node): void => {
129
+ if (node !== scope && isFunctionLikeScope(node)) return;
130
+ if (node.getStart(source) >= identifier.getStart(source)) return;
131
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === identifier.text && !nestedFunctionContains(scope, node, use)) best = node;
132
+ if (ts.isParameter(node) && ts.isIdentifier(node.name) && node.name.text === identifier.text && !nestedFunctionContains(scope, node, use)) best = node;
133
+ ts.forEachChild(node, visit);
134
+ };
135
+ visit(scope);
136
+ if (!best) return { immutable: false, evidence: ['binding_not_found'] };
137
+ const immutable = ts.isVariableDeclaration(best) && (best.parent.flags & ts.NodeFlags.Const) !== 0;
138
+ return { declaration: best, initializer: ts.isVariableDeclaration(best) ? best.initializer : undefined, immutable, evidence: [immutable ? 'const_binding_before_use' : 'mutable_or_parameter_binding'] };
139
+ }
140
+ function resolveExpression(expr: ts.Expression | undefined, use: ts.Node, policy: 'operation_path' | 'external' | 'literal', depth = 0, seen = new Set<ts.Node>()): ExpressionResolution {
141
+ if (!expr) return { status: 'unknown', sourceKind: 'dynamic_expression', placeholderKeys: [], evidence: ['expression_missing'] };
142
+ if (ts.isStringLiteral(expr)) return { status: 'static', sourceKind: 'string_literal', value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['string_literal'] };
143
+ if (ts.isNoSubstitutionTemplateLiteral(expr)) return { status: 'static', sourceKind: 'no_substitution_template', value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['no_substitution_template'] };
144
+ if (ts.isTemplateExpression(expr)) {
145
+ const keys = placeholders(expr);
146
+ 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'] };
147
+ return { status: 'dynamic', sourceKind: 'template_with_substitutions', placeholderKeys: keys, evidence: ['template_substitutions_not_static_external_target'] };
148
+ }
149
+ if (ts.isIdentifier(expr)) {
150
+ if (depth >= maxAliasDepth) return { status: 'unknown', sourceKind: 'const_alias', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['alias_depth_exceeded'], constName: expr.text };
151
+ const binding = resolveBinding(expr, use);
152
+ if (!binding.declaration || !binding.initializer || !binding.immutable) return { status: 'dynamic', sourceKind: 'dynamic_expression', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: binding.evidence, constName: expr.text };
153
+ if (seen.has(binding.declaration)) return { status: 'unknown', sourceKind: 'const_alias', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['alias_cycle_detected'], constName: expr.text };
154
+ seen.add(binding.declaration);
155
+ const resolved = resolveExpression(binding.initializer, binding.declaration, policy, depth + 1, seen);
156
+ return { ...resolved, sourceKind: 'const_alias', rawExpression: safeRaw(expr), constName: expr.text, evidence: [...binding.evidence, ...resolved.evidence] };
157
+ }
158
+ return { status: 'dynamic', sourceKind: 'dynamic_expression', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: [`unsupported_${ts.SyntaxKind[expr.kind] ?? 'expression'}`] };
159
+ }
92
160
  function staticExpressionText(expr: ts.Expression | undefined, initializers: Map<string, ts.Expression>): string | undefined {
93
161
  if (!expr) return undefined;
94
162
  if (isStringLike(expr)) return expr.text;
95
- if (ts.isTemplateExpression(expr)) return stripQuotes(expr.getText(expr.getSourceFile()));
96
163
  if (ts.isIdentifier(expr) && initializers.has(expr.text)) return staticExpressionText(initializers.get(expr.text), initializers);
97
164
  return undefined;
98
165
  }
99
- interface StaticPathResolution { text?: string; sourceKind: 'literal' | 'template' | 'const' | 'dynamic'; rawExpression?: string; constName?: string }
100
- function staticPathExpression(expr: ts.Expression | undefined, initializers: Map<string, ts.Expression>): StaticPathResolution {
101
- if (!expr) return { sourceKind: 'dynamic' };
102
- const rawExpression = expr.getText(expr.getSourceFile());
103
- if (isStringLike(expr)) return { text: expr.text, sourceKind: 'literal', rawExpression };
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 };
166
+ interface StaticPathResolution { text?: string; sourceKind: 'literal' | 'template' | 'const' | 'dynamic'; rawExpression?: string; constName?: string; resolution: ExpressionResolution }
167
+ function staticPathExpression(expr: ts.Expression | undefined, use: ts.Node): StaticPathResolution {
168
+ const resolution = resolveExpression(expr, use, 'operation_path');
169
+ const sourceKind = resolution.sourceKind === 'const_alias' ? 'const' : resolution.sourceKind === 'template_with_substitutions' || resolution.sourceKind === 'no_substitution_template' ? 'template' : resolution.status === 'static' ? 'literal' : 'dynamic';
170
+ return { text: resolution.value, sourceKind, rawExpression: resolution.rawExpression, constName: resolution.constName, resolution };
110
171
  }
111
172
  function operationPathFromStatic(text: string | undefined): string | undefined {
112
173
  return text ? `/${stripQuotes(text).replace(/^\//, '')}` : undefined;
113
174
  }
114
- interface PathCandidateEvidence { candidatePaths: string[]; normalizedCandidateOperations: string[]; candidateSourceKind: string; candidateIdentifier: string; hasDynamicAssignments: boolean }
115
- function staticPathCandidates(source: ts.SourceFile, identifier: string, initializers: Map<string, ts.Expression>): PathCandidateEvidence | undefined {
175
+ interface PathCandidateEvidence { candidatePaths: string[]; normalizedCandidateOperations: string[]; candidateSourceKind: string; candidateIdentifier: string; hasDynamicAssignments: boolean; conservativeReason?: string }
176
+ function staticPathCandidates(identifier: ts.Identifier, call: ts.Node, method: string): PathCandidateEvidence | undefined {
177
+ const binding = resolveBinding(identifier, call);
178
+ const declaration = binding.declaration;
179
+ if (!declaration) return undefined;
180
+ const scope = containingScope(call);
181
+ const source = call.getSourceFile();
116
182
  const paths: string[] = [];
117
183
  let hasDynamicAssignments = false;
184
+ const addExpr = (expr: ts.Expression | undefined, origin: ts.Node): void => {
185
+ const resolved = staticPathExpression(expr, origin);
186
+ if (resolved.text) paths.push(operationPathFromStatic(resolved.text) ?? resolved.text);
187
+ else hasDynamicAssignments = true;
188
+ };
189
+ if (ts.isVariableDeclaration(declaration)) addExpr(declaration.initializer, declaration);
118
190
  const visit = (node: ts.Node): void => {
119
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === identifier && node.initializer) {
120
- const resolved = staticPathExpression(node.initializer, initializers);
121
- if (resolved.text) paths.push(operationPathFromStatic(resolved.text) ?? resolved.text);
122
- else hasDynamicAssignments = true;
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;
128
- }
191
+ if (node !== scope && isFunctionLikeScope(node)) return;
192
+ if (node.getStart(source) >= call.getStart(source)) return;
193
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(node.left) && node.left.text === identifier.text) addExpr(node.right, node);
129
194
  ts.forEachChild(node, visit);
130
195
  };
131
- visit(source);
196
+ visit(scope);
132
197
  const candidatePaths = [...new Set(paths)];
133
198
  if (candidatePaths.length === 0) return undefined;
134
- const normalizedCandidateOperations = [...new Set(candidatePaths.map((candidate) => classifyODataPathIntent(candidate, 'POST').topLevelOperationName).filter((value): value is string => Boolean(value)))];
135
- return { candidatePaths, normalizedCandidateOperations, candidateSourceKind: 'static candidates observed', candidateIdentifier: identifier, hasDynamicAssignments };
199
+ const normalizedCandidateOperations = [...new Set(candidatePaths.map((candidate) => classifyODataPathIntent(candidate, method).topLevelOperationName).filter((value): value is string => Boolean(value)))];
200
+ 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
201
  }
137
202
  function destinationExpressionShape(expr: ts.Expression | undefined): string | undefined {
138
203
  if (!expr) return undefined;
@@ -159,57 +224,59 @@ function propertyInitializer(object: ts.ObjectLiteralExpression, key: string): t
159
224
  }
160
225
  return undefined;
161
226
  }
162
- function httpMethodFromObject(object: ts.ObjectLiteralExpression, initializers: Map<string, ts.Expression>): string | undefined {
163
- const text = staticExpressionText(propertyInitializer(object, 'method'), initializers);
227
+ function httpMethodFromObject(object: ts.ObjectLiteralExpression, use: ts.Node): string | undefined {
228
+ const text = resolveExpression(propertyInitializer(object, 'method'), use, 'literal').value;
164
229
  return text ? stripQuotes(text).toUpperCase() : undefined;
165
230
  }
166
- function urlTargetFromExpression(expr: ts.Expression | undefined, initializers: Map<string, ts.Expression>): Record<string, unknown> {
167
- const text = staticExpressionText(expr, initializers);
168
- if (text) return { kind: 'static_url', expression: text, dynamic: false };
169
- if (expr && (ts.isTemplateExpression(expr) || ts.isIdentifier(expr) || ts.isPropertyAccessExpression(expr) || ts.isCallExpression(expr))) return { kind: 'url_expression', expression: expr.getText(expr.getSourceFile()), dynamic: true };
231
+ function hasTemplatePlaceholder(value: string): boolean { return /\$\{|%7B|%7D/i.test(value); }
232
+ function urlTargetFromExpression(expr: ts.Expression | undefined, use: ts.Node): Record<string, unknown> {
233
+ const resolved = resolveExpression(expr, use, 'external');
234
+ if (resolved.status === 'static' && resolved.value && !hasTemplatePlaceholder(resolved.value)) return { kind: 'static_url', expression: resolved.value, dynamic: false, sourceKind: resolved.sourceKind };
235
+ if (expr) return { kind: 'url_expression', dynamic: true, expression: `${resolved.sourceKind}:${resolved.placeholderKeys.join('|')}`, expressionShape: resolved.sourceKind, placeholderKeys: resolved.placeholderKeys };
170
236
  return { kind: 'unknown', dynamic: false };
171
237
  }
172
- function destinationTargetFromExpression(expr: ts.Expression | undefined, initializers: Map<string, ts.Expression>): Record<string, unknown> | undefined {
173
- const text = staticExpressionText(expr, initializers);
174
- if (text) return { kind: 'destination', expression: text, dynamic: false };
175
- const candidates = staticConditionalCandidates(expr, initializers);
238
+ function destinationTargetFromExpression(expr: ts.Expression | undefined, use: ts.Node): Record<string, unknown> | undefined {
239
+ const resolved = resolveExpression(expr, use, 'external');
240
+ const text = resolved.value;
241
+ if (resolved.status === 'static' && text && !hasTemplatePlaceholder(text)) return { kind: 'destination', expression: text, dynamic: false, sourceKind: resolved.sourceKind };
242
+ const candidates = staticConditionalCandidates(expr, new Map<string, ts.Expression>());
176
243
  if (candidates) return { kind: 'destination', dynamic: true, expressionShape: 'conditional', candidateLiterals: candidates };
177
244
  const shape = destinationExpressionShape(expr);
178
245
  if (shape) return { kind: 'destination', dynamic: true, expressionShape: shape };
179
246
  return undefined;
180
247
  }
181
- function externalHttpEvidence(node: ts.CallExpression, source: ts.SourceFile, initializers: Map<string, ts.Expression>): { method?: string; externalTarget: Record<string, unknown>; classifier: string; sourceCallShape: string } | undefined {
248
+ function externalHttpEvidence(node: ts.CallExpression, source: ts.SourceFile): { method?: string; externalTarget: Record<string, unknown>; classifier: string; sourceCallShape: string } | undefined {
182
249
  const expr = node.expression;
183
250
  const exprText = expr.getText(source);
184
251
  if (exprText === 'useOrFetchDestination') {
185
252
  const objectArg = node.arguments[0];
186
253
  if (objectArg && ts.isObjectLiteralExpression(objectArg)) {
187
- const destination = destinationTargetFromExpression(propertyInitializer(objectArg, 'destinationName'), initializers);
254
+ const destination = destinationTargetFromExpression(propertyInitializer(objectArg, 'destinationName'), node);
188
255
  return { externalTarget: destination ?? { kind: 'unknown', dynamic: false }, classifier: 'sap_destination_lookup', sourceCallShape: 'useOrFetchDestination' };
189
256
  }
190
257
  }
191
258
  if (exprText === 'executeHttpRequest') {
192
- const destination = destinationTargetFromExpression(node.arguments[0], initializers);
259
+ const destination = destinationTargetFromExpression(node.arguments[0], node);
193
260
  const config = node.arguments[1];
194
- const method = config && ts.isObjectLiteralExpression(config) ? httpMethodFromObject(config, initializers) : undefined;
195
- const url = config && ts.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, 'url'), initializers) : { kind: 'unknown', dynamic: false };
261
+ const method = config && ts.isObjectLiteralExpression(config) ? httpMethodFromObject(config, node) : undefined;
262
+ const url = config && ts.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, 'url'), node) : { kind: 'unknown', dynamic: false };
196
263
  return { method, externalTarget: destination ? { ...url, destination } : url, classifier: 'sap_execute_http_request', sourceCallShape: 'executeHttpRequest' };
197
264
  }
198
265
  if (exprText === 'axios') {
199
266
  const config = node.arguments[0];
200
267
  if (config && ts.isObjectLiteralExpression(config)) {
201
- const method = httpMethodFromObject(config, initializers);
202
- return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, 'url'), initializers), classifier: 'axios_config_call', sourceCallShape: 'axios(config)' };
268
+ const method = httpMethodFromObject(config, node);
269
+ return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, 'url'), node), classifier: 'axios_config_call', sourceCallShape: 'axios(config)' };
203
270
  }
204
271
  return { externalTarget: { kind: 'unknown', dynamic: false }, classifier: 'axios_unknown_call', sourceCallShape: 'axios(...)' };
205
272
  }
206
273
  if (exprText === 'fetch') {
207
274
  const init = node.arguments[1];
208
- const method = init && ts.isObjectLiteralExpression(init) ? httpMethodFromObject(init, initializers) : undefined;
209
- return { method, externalTarget: urlTargetFromExpression(node.arguments[0], initializers), classifier: 'fetch_call', sourceCallShape: 'fetch' };
275
+ const method = init && ts.isObjectLiteralExpression(init) ? httpMethodFromObject(init, node) : undefined;
276
+ return { method, externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: 'fetch_call', sourceCallShape: 'fetch' };
210
277
  }
211
278
  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], initializers), classifier: 'axios_member_call', sourceCallShape: `axios.${expr.name.text}` };
279
+ return { method: expr.name.text.toUpperCase(), externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: 'axios_member_call', sourceCallShape: `axios.${expr.name.text}` };
213
280
  }
214
281
  return undefined;
215
282
  }
@@ -274,7 +341,7 @@ function collectWrapperSpecs(source: ts.SourceFile): Map<string, WrapperSpec> {
274
341
  const methodProp = propertyInitializer(objectArg, 'method');
275
342
  const pathName = pathProp && ts.isIdentifier(pathProp) ? pathProp.text : undefined;
276
343
  const methodName = methodProp && ts.isIdentifier(methodProp) ? methodProp.text : undefined;
277
- const methodLiteral = staticExpressionText(methodProp, variableInitializers(source));
344
+ const methodLiteral = resolveExpression(methodProp, node, 'literal').value;
278
345
  if (pathName) sends.push({ client: node.expression.expression.text, path: pathName, method: methodName, methodLiteral, start: node.getStart(source), end: node.getEnd() });
279
346
  }
280
347
  }
@@ -324,12 +391,12 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
324
391
  if (objectArg && ts.isObjectLiteralExpression(objectArg)) {
325
392
  const receiver = receiverName(expr.expression);
326
393
  const query = objectPropertyText(objectArg, 'query');
327
- const method = stripQuotes(objectPropertyText(objectArg, 'method') ?? 'POST');
394
+ const method = stripQuotes(resolveExpression(propertyInitializer(objectArg, 'method'), node, 'literal').value ?? objectPropertyText(objectArg, 'method') ?? 'POST');
328
395
  const pathExpr = propertyInitializer(objectArg, 'path') ?? propertyInitializer(objectArg, 'event');
329
- const resolvedPath = staticPathExpression(pathExpr, initializers);
396
+ const resolvedPath = staticPathExpression(pathExpr, node);
330
397
  const op = pathExpr ? resolvedPath.text ?? pathExpr.getText(source) : undefined;
331
398
  const shorthandPath = objectPropertyIsShorthand(objectArg, 'path');
332
- const candidateEvidence = pathExpr && ts.isIdentifier(pathExpr) && !resolvedPath.text ? staticPathCandidates(source, pathExpr.text, initializers) : undefined;
399
+ const candidateEvidence = pathExpr && ts.isIdentifier(pathExpr) && !resolvedPath.text ? staticPathCandidates(pathExpr, node, method) : undefined;
333
400
  const candidateOperationPath = candidateEvidence && !candidateEvidence.hasDynamicAssignments && candidateEvidence.normalizedCandidateOperations.length === 1 && candidateEvidence.candidatePaths.length > 0 ? candidateEvidence.candidatePaths[0] : undefined;
334
401
  const operationPathExpr = operationPathFromStatic(resolvedPath.text) ?? candidateOperationPath;
335
402
  const intent = classifyODataPathIntent(operationPathExpr, method);
@@ -346,8 +413,8 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
346
413
  const pathArg = spec ? wrapperArgs[spec.pathIndex] : undefined;
347
414
  const methodArg = spec?.methodIndex === undefined ? undefined : wrapperArgs[spec.methodIndex];
348
415
  const receiver = clientArg && ts.isIdentifier(clientArg) ? clientArg.text : spec?.clientName;
349
- const method = stripQuotes(staticExpressionText(methodArg, initializers) ?? spec?.methodLiteral ?? 'POST');
350
- const resolvedPath = staticPathExpression(pathArg, initializers);
416
+ const method = stripQuotes(resolveExpression(methodArg, node, 'literal').value ?? spec?.methodLiteral ?? 'POST');
417
+ const resolvedPath = staticPathExpression(pathArg, node);
351
418
  const operationPathExpr = operationPathFromStatic(resolvedPath.text);
352
419
  const normalizedOperationPath = operationPathExpr ? classifyODataPathIntent(operationPathExpr, method).topLevelOperationName : undefined;
353
420
  if (spec && receiver && operationPathExpr) {
@@ -363,7 +430,7 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
363
430
  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
431
  }
365
432
  } else {
366
- const external = externalHttpEvidence(node, source, initializers);
433
+ const external = externalHttpEvidence(node, source);
367
434
  if (external) {
368
435
  const evidenceTarget = { ...external.externalTarget, method: external.method, parserClassifier: external.classifier, sourceCallShape: external.sourceCallShape };
369
436
  const safeTarget = externalHttpTarget({ method: external.method, evidence_json: JSON.stringify({ externalTarget: evidenceTarget }) });