@saptools/service-flow 0.1.36 → 0.1.38

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.
Files changed (49) hide show
  1. package/dist/{chunk-2UOIY2NI.js → chunk-WE3A6TOJ.js} +59 -38
  2. package/dist/chunk-WE3A6TOJ.js.map +1 -0
  3. package/dist/cli.js +4 -3
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.js +1 -1
  6. package/package.json +3 -2
  7. package/src/cli.ts +865 -0
  8. package/src/config/defaults.ts +14 -0
  9. package/src/config/workspace-config.ts +61 -0
  10. package/src/db/connection.ts +131 -0
  11. package/src/db/migrations.ts +81 -0
  12. package/src/db/repositories.ts +353 -0
  13. package/src/db/schema.ts +24 -0
  14. package/src/discovery/classify-repository.ts +50 -0
  15. package/src/discovery/discover-repositories.ts +58 -0
  16. package/src/index.ts +13 -0
  17. package/src/indexer/incremental-index.ts +25 -0
  18. package/src/indexer/repository-indexer.ts +137 -0
  19. package/src/indexer/workspace-indexer.ts +29 -0
  20. package/src/linker/cross-repo-linker.ts +406 -0
  21. package/src/linker/dynamic-edge-resolver.ts +45 -0
  22. package/src/linker/external-http-target.ts +38 -0
  23. package/src/linker/helper-package-linker.ts +57 -0
  24. package/src/linker/odata-path-normalizer.ts +236 -0
  25. package/src/linker/operation-decorator-normalizer.ts +47 -0
  26. package/src/linker/remote-query-target.ts +39 -0
  27. package/src/linker/service-resolver.ts +223 -0
  28. package/src/output/json-output.ts +7 -0
  29. package/src/output/mermaid-output.ts +16 -0
  30. package/src/output/table-output.ts +21 -0
  31. package/src/parsers/cds-parser.ts +147 -0
  32. package/src/parsers/decorator-parser.ts +76 -0
  33. package/src/parsers/generated-constants-parser.ts +23 -0
  34. package/src/parsers/handler-registration-parser.ts +177 -0
  35. package/src/parsers/outbound-call-parser.ts +398 -0
  36. package/src/parsers/package-json-parser.ts +63 -0
  37. package/src/parsers/service-binding-parser.ts +826 -0
  38. package/src/parsers/symbol-parser.ts +328 -0
  39. package/src/parsers/ts-project.ts +13 -0
  40. package/src/trace/selectors.ts +20 -0
  41. package/src/trace/trace-engine.ts +647 -0
  42. package/src/trace/traversal.ts +4 -0
  43. package/src/types.ts +186 -0
  44. package/src/utils/diagnostics.ts +10 -0
  45. package/src/utils/hashing.ts +10 -0
  46. package/src/utils/path-utils.ts +13 -0
  47. package/src/utils/redaction.ts +20 -0
  48. package/src/version.ts +4 -0
  49. package/dist/chunk-2UOIY2NI.js.map +0 -1
@@ -0,0 +1,328 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import ts from 'typescript';
4
+ import type { ExecutableSymbolFact, SymbolCallFact } from '../types.js';
5
+ import { containsSupportedOutboundCall } from './outbound-call-parser.js';
6
+ import { normalizePath } from '../utils/path-utils.js';
7
+
8
+ function lineOf(source: ts.SourceFile, pos: number): number {
9
+ return source.getLineAndCharacterOfPosition(pos).line + 1;
10
+ }
11
+ function nameOf(node: ts.PropertyName | ts.BindingName | undefined): string | undefined {
12
+ if (!node) return undefined;
13
+ if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return node.text;
14
+ return undefined;
15
+ }
16
+ function isFunctionLike(node: ts.Node): node is ts.FunctionLikeDeclaration {
17
+ return ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node);
18
+ }
19
+ function exported(node: ts.Node): boolean {
20
+ return Boolean(ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export);
21
+ }
22
+ function isPublicClassMethod(node: ts.MethodDeclaration): boolean {
23
+ const flags = ts.getCombinedModifierFlags(node);
24
+ return (flags & ts.ModifierFlags.Private) === 0 && (flags & ts.ModifierFlags.Protected) === 0;
25
+ }
26
+ function exportDeclarations(source: ts.SourceFile): Map<string, string> {
27
+ const exports = new Map<string, string>();
28
+ const visit = (node: ts.Node): void => {
29
+ if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) {
30
+ for (const el of node.exportClause.elements) exports.set((el.propertyName ?? el.name).text, el.name.text);
31
+ }
32
+ ts.forEachChild(node, visit);
33
+ };
34
+ visit(source);
35
+ return exports;
36
+ }
37
+ function isRelativeImport(value: string | undefined): boolean {
38
+ return Boolean(value?.startsWith('.'));
39
+ }
40
+ function isObjectFunction(node: ts.Node): boolean {
41
+ return ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node);
42
+ }
43
+ type ParameterBinding =
44
+ | { index: number; kind: 'identifier'; name: string }
45
+ | { index: number; kind: 'object_pattern'; properties: Array<{ property: string; local: string }> };
46
+ type ParameterPropertyAlias = { parameter: string; property: string; local: string; kind: 'object_parameter_destructure'; line: number };
47
+ const commonTerminalMembers = new Set(['push', 'includes', 'find', 'findIndex', 'map', 'filter', 'reduce', 'forEach', 'some', 'every', 'toUpperCase', 'toLowerCase', 'trim', 'split', 'join', 'get', 'set', 'has']);
48
+ const loggerMembers = new Set(['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'log']);
49
+ const globalObjects = new Set(['JSON', 'Object', 'Array', 'String', 'Number', 'Boolean', 'Math', 'Date', 'Promise', 'Reflect']);
50
+ const builtInConstructors = new Set([
51
+ 'Set', 'Map', 'WeakSet', 'WeakMap',
52
+ 'Date', 'RegExp', 'URL', 'URLSearchParams',
53
+ 'Error', 'EvalError', 'RangeError', 'ReferenceError', 'SyntaxError', 'TypeError', 'URIError', 'AggregateError',
54
+ 'ArrayBuffer', 'SharedArrayBuffer', 'DataView',
55
+ 'Int8Array', 'Uint8Array', 'Uint8ClampedArray', 'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array',
56
+ 'Float32Array', 'Float64Array', 'BigInt64Array', 'BigUint64Array',
57
+ 'Promise', 'AbortController',
58
+ ]);
59
+ const capDslRoots = new Set(['SELECT', 'INSERT', 'UPSERT', 'UPDATE', 'DELETE']);
60
+ const requestHelpers = new Set(['reject', 'error', 'info', 'warn', 'notify']);
61
+ const transportMembers = new Set(['emit', 'publish', 'send', 'on']);
62
+ function callName(expr: ts.Expression): { expression: string; local?: string; member?: string; receiver?: string } {
63
+ if (ts.isIdentifier(expr)) return { expression: expr.text, local: expr.text };
64
+ if (ts.isPropertyAccessExpression(expr)) {
65
+ const left = expr.expression.getText();
66
+ const root = left.split('.')[0];
67
+ return { expression: expr.getText(), local: left === 'this' ? undefined : root, member: expr.name.text, receiver: left };
68
+ }
69
+ return { expression: expr.getText() };
70
+ }
71
+ function requireSource(expr: ts.Expression): string | undefined {
72
+ if (!ts.isCallExpression(expr) || !ts.isIdentifier(expr.expression) || expr.expression.text !== 'require') return undefined;
73
+ const first = expr.arguments[0];
74
+ return first && ts.isStringLiteral(first) ? first.text : undefined;
75
+ }
76
+ function ignoredFrameworkCall(callee: { expression: string; local?: string; member?: string; receiver?: string }): boolean {
77
+ if (callee.local && capDslRoots.has(callee.local)) return true;
78
+ if (callee.expression === 'cds.run' || callee.expression.startsWith('cds.connect.') || callee.expression.startsWith('cds.services.') || callee.expression.startsWith('cds.parse.')) return true;
79
+ if (callee.local === 'req' && callee.member && requestHelpers.has(callee.member)) return true;
80
+ if (callee.member && transportMembers.has(callee.member)) return true;
81
+ if (callee.local && globalObjects.has(callee.local)) return true;
82
+ if (callee.expression.startsWith('new Date().')) return true;
83
+ return false;
84
+ }
85
+ function nearest(symbols: ExecutableSymbolFact[], line: number): ExecutableSymbolFact | undefined {
86
+ return symbols.filter((s) => s.startLine <= line && s.endLine >= line).sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
87
+ }
88
+ function argumentEvidence(args: ts.NodeArray<ts.Expression>, source: ts.SourceFile): Array<Record<string, unknown>> {
89
+ return args.map((arg) => {
90
+ if (ts.isIdentifier(arg)) return { kind: 'identifier', name: arg.text };
91
+ if (ts.isObjectLiteralExpression(arg)) {
92
+ const properties: Array<Record<string, unknown>> = [];
93
+ for (const prop of arg.properties) {
94
+ if (ts.isShorthandPropertyAssignment(prop)) properties.push({ kind: 'shorthand', property: prop.name.text, argument: prop.name.text });
95
+ if (ts.isPropertyAssignment(prop)) {
96
+ const propName = nameOf(prop.name);
97
+ if (propName && ts.isIdentifier(prop.initializer)) properties.push({ kind: 'property_assignment', property: propName, argument: prop.initializer.text });
98
+ }
99
+ }
100
+ return { kind: 'object_literal', properties };
101
+ }
102
+ return { kind: 'unsupported', expression: arg.getText(source) };
103
+ });
104
+ }
105
+ function bindingLocalName(name: ts.BindingName, initializer?: ts.Expression): string | undefined {
106
+ if (ts.isIdentifier(name)) return name.text;
107
+ if (initializer && ts.isIdentifier(initializer)) return initializer.text;
108
+ return undefined;
109
+ }
110
+
111
+ function objectPatternAliases(pattern: ts.ObjectBindingPattern, parameter: string, source: ts.SourceFile, lineNode: ts.Node): ParameterPropertyAlias[] {
112
+ return pattern.elements.flatMap((element): ParameterPropertyAlias[] => {
113
+ if (element.dotDotDotToken || ts.isObjectBindingPattern(element.name) || ts.isArrayBindingPattern(element.name)) return [];
114
+ const property = element.propertyName ? nameOf(element.propertyName) : nameOf(element.name);
115
+ if (!property) return [];
116
+ const local = bindingLocalName(element.name, element.initializer);
117
+ return local ? [{ parameter, property, local, kind: 'object_parameter_destructure', line: lineOf(source, lineNode.getStart(source)) }] : [];
118
+ });
119
+ }
120
+ function parameterPropertyAliases(fn: ts.FunctionLikeDeclaration, source: ts.SourceFile): ParameterPropertyAlias[] {
121
+ const parameterNames = new Set(fn.parameters.flatMap((param) => ts.isIdentifier(param.name) ? [param.name.text] : []));
122
+ if (!fn.body || parameterNames.size === 0) return [];
123
+ const aliases: ParameterPropertyAlias[] = [];
124
+ const addFromAssignment = (left: ts.Expression, right: ts.Expression, node: ts.Node): void => {
125
+ if (!ts.isObjectLiteralExpression(left) || !ts.isIdentifier(right) || !parameterNames.has(right.text)) return;
126
+ for (const prop of left.properties) {
127
+ if (!ts.isPropertyAssignment(prop)) continue;
128
+ const property = nameOf(prop.name);
129
+ if (property && ts.isIdentifier(prop.initializer)) aliases.push({ parameter: right.text, property, local: prop.initializer.text, kind: 'object_parameter_destructure', line: lineOf(source, node.getStart(source)) });
130
+ }
131
+ };
132
+ const visit = (node: ts.Node): void => {
133
+ if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && node.initializer && ts.isIdentifier(node.initializer) && parameterNames.has(node.initializer.text)) aliases.push(...objectPatternAliases(node.name, node.initializer.text, source, node));
134
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) addFromAssignment(ts.isParenthesizedExpression(node.left) ? node.left.expression : node.left, node.right, node);
135
+ ts.forEachChild(node, visit);
136
+ };
137
+ visit(fn.body);
138
+ const seen = new Set<string>();
139
+ return aliases.filter((alias) => { const key = `${alias.parameter}.${alias.property}:${alias.local}`; if (seen.has(key)) return false; seen.add(key); return true; });
140
+ }
141
+ function parameterBindings(params: ts.NodeArray<ts.ParameterDeclaration>): ParameterBinding[] {
142
+ return params.flatMap((param, index): ParameterBinding[] => {
143
+ if (ts.isIdentifier(param.name)) return [{ index, kind: 'identifier', name: param.name.text }];
144
+ if (!ts.isObjectBindingPattern(param.name)) return [];
145
+ const properties = param.name.elements.flatMap((element): Array<{ property: string; local: string }> => {
146
+ if (element.dotDotDotToken || ts.isObjectBindingPattern(element.name) || ts.isArrayBindingPattern(element.name)) return [];
147
+ const property = element.propertyName ? nameOf(element.propertyName) : nameOf(element.name);
148
+ if (!property) return [];
149
+ const local = bindingLocalName(element.name, element.initializer);
150
+ return local ? [{ property, local }] : [];
151
+ });
152
+ return properties.length > 0 ? [{ index, kind: 'object_pattern', properties }] : [];
153
+ });
154
+ }
155
+ export async function parseExecutableSymbols(repoPath: string, filePath: string): Promise<{ symbols: ExecutableSymbolFact[]; calls: SymbolCallFact[] }> {
156
+ const text = await fs.readFile(path.join(repoPath, filePath), 'utf8');
157
+ const source = ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS);
158
+ const sourceFile = normalizePath(filePath);
159
+ const symbols: ExecutableSymbolFact[] = [];
160
+ const calls: SymbolCallFact[] = [];
161
+ const imports = new Map<string, string>();
162
+ const exportNames = exportDeclarations(source);
163
+ const objectExports = new Set<string>();
164
+ const exportedClasses = new Set<string>();
165
+ const declaredClasses = new Set<string>();
166
+ const proxyVariables = new Map<string, { importSource: string; factory: string; variableName: string }>();
167
+ const classInstances = new Map<string, { className: string; importSource?: string; propertyName?: string }>();
168
+ const addSymbol = (kind: string, localName: string, node: ts.Node, parentName?: string, exportedName?: string, evidence?: Record<string, unknown>): void => {
169
+ const parentRoot = parentName?.split('.')[0] ?? '';
170
+ const declaredExportName = exportedName ?? exportNames.get(parentName ? parentRoot : localName);
171
+ const qualifiedName = parentName ? `${parentName}.${localName}` : localName;
172
+ const objectExported = parentName ? objectExports.has(parentRoot) : false;
173
+ const classMemberExported = kind === 'method' && parentName ? exportedClasses.has(parentRoot) && ts.isMethodDeclaration(node) && (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Static) !== 0 && isPublicClassMethod(node) : false;
174
+ const effectiveExportedName = classMemberExported || objectExported ? qualifiedName : declaredExportName;
175
+ const bindings = isFunctionLike(node) ? parameterBindings(node.parameters) : undefined;
176
+ const params = bindings?.flatMap((binding) => binding.kind === 'identifier' ? [binding.name] : []);
177
+ const sourceEvidence = evidence ?? (classMemberExported ? { source: 'exported_class_member', exportedClass: parentRoot, memberKind: (ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Static) !== 0 ? 'static_method' : 'class_method', parameters: params } : declaredExportName ? { exportedName: declaredExportName, source: 'export_declaration' } : objectExported ? { exportedName: qualifiedName, source: 'exported_object_literal' } : undefined);
178
+ const aliases = isFunctionLike(node) ? parameterPropertyAliases(node, source) : [];
179
+ const parameterEvidence = { ...(bindings && bindings.length > 0 ? { parameters: params, parameterBindings: bindings } : {}), ...(aliases.length > 0 ? { parameterPropertyAliases: aliases } : {}) };
180
+ symbols.push({ kind, localName: kind === 'object_method' ? qualifiedName : localName, exportedName: effectiveExportedName, qualifiedName, sourceFile, startLine: lineOf(source, node.getStart(source)), endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: exported(node) || Boolean(effectiveExportedName), importExportEvidence: sourceEvidence ? { ...sourceEvidence, ...parameterEvidence } : bindings && bindings.length > 0 ? parameterEvidence : undefined });
181
+ };
182
+ const addAliasSymbol = (objectName: string, propertyName: string, node: ts.Node, targetImportSource?: string): void => {
183
+ symbols.push({ kind: 'object_alias', localName: propertyName, exportedName: propertyName, qualifiedName: `${objectName}.${propertyName}`, sourceFile, startLine: lineOf(source, node.getStart(source)), endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: true, importExportEvidence: { source: 'exported_object_shorthand', objectName, propertyName, targetImportSource } });
184
+ };
185
+ const visitImports = (node: ts.Node): void => {
186
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
187
+ const sourceText = node.moduleSpecifier.text;
188
+ const clause = node.importClause;
189
+ if (clause?.name) imports.set(clause.name.text, sourceText);
190
+ const named = clause?.namedBindings;
191
+ if (named && ts.isNamedImports(named)) for (const el of named.elements) imports.set(el.name.text, sourceText);
192
+ if (named && ts.isNamespaceImport(named)) imports.set(named.name.text, sourceText);
193
+ }
194
+ if (ts.isVariableStatement(node)) {
195
+ for (const declaration of node.declarationList.declarations) {
196
+ const requiredSource = declaration.initializer ? requireSource(declaration.initializer) : undefined;
197
+ if (ts.isIdentifier(declaration.name) && requiredSource) imports.set(declaration.name.text, requiredSource);
198
+ if (ts.isObjectBindingPattern(declaration.name) && requiredSource) for (const element of declaration.name.elements) if (ts.isIdentifier(element.name)) imports.set(element.name.text, requiredSource);
199
+ }
200
+ }
201
+ ts.forEachChild(node, visitImports);
202
+ };
203
+ visitImports(source);
204
+ const visitSymbols = (node: ts.Node, parentClass?: string): void => {
205
+ if (ts.isClassDeclaration(node) && node.name) {
206
+ declaredClasses.add(node.name.text);
207
+ if (exported(node) || exportNames.has(node.name.text)) exportedClasses.add(node.name.text);
208
+ for (const member of node.members) visitSymbols(member, node.name.text);
209
+ return;
210
+ }
211
+ if (ts.isMethodDeclaration(node)) {
212
+ const localName = nameOf(node.name);
213
+ if (localName) addSymbol('method', localName, node, parentClass);
214
+ } else if (ts.isPropertyDeclaration(node) && parentClass && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
215
+ const localName = nameOf(node.name);
216
+ if (localName) addSymbol('method', localName, node.initializer, parentClass, undefined, { source: 'class_property_function', memberKind: ts.isArrowFunction(node.initializer) ? 'arrow_function_property' : 'function_expression_property' });
217
+ } else if (ts.isFunctionDeclaration(node) && node.name) addSymbol('function', node.name.text, node, undefined, exported(node) ? node.name.text : undefined);
218
+ else if (ts.isVariableStatement(node)) {
219
+ for (const d of node.declarationList.declarations) {
220
+ const localName = nameOf(d.name);
221
+ if (!localName || !d.initializer) continue;
222
+ if (isFunctionLike(d.initializer)) addSymbol('function', localName, d.initializer, undefined, exported(node) ? localName : exportNames.get(localName));
223
+ if (ts.isObjectLiteralExpression(d.initializer)) {
224
+ const objectIsExported = exported(node) || exportNames.has(localName);
225
+ if (objectIsExported) objectExports.add(localName);
226
+ for (const prop of d.initializer.properties) {
227
+ if (objectIsExported && ts.isShorthandPropertyAssignment(prop)) addAliasSymbol(localName, prop.name.text, prop.name, imports.get(prop.name.text));
228
+ if (ts.isPropertyAssignment(prop) && isObjectFunction(prop.initializer)) {
229
+ const propName = nameOf(prop.name);
230
+ if (propName) addSymbol('object_method', propName, prop.initializer, localName);
231
+ } else if (ts.isMethodDeclaration(prop)) {
232
+ const propName = nameOf(prop.name);
233
+ if (propName) addSymbol('object_method', propName, prop, localName);
234
+ }
235
+ }
236
+ }
237
+ }
238
+ } else ts.forEachChild(node, (child) => visitSymbols(child, parentClass));
239
+ };
240
+ visitSymbols(source);
241
+
242
+ const isTopLevelCallback = (node: ts.Node): node is ts.ArrowFunction | ts.FunctionExpression => {
243
+ if (!ts.isArrowFunction(node) && !ts.isFunctionExpression(node)) return false;
244
+ if (!ts.isCallExpression(node.parent)) return false;
245
+ const callee = callName(node.parent.expression);
246
+ const member = callee.member ?? callee.local;
247
+ return Boolean(member && ['bootstrap', 'served', 'connect', 'on', 'once', 'use', 'get', 'post', 'put', 'patch', 'delete', 'subscribe'].includes(member));
248
+ };
249
+ const visitCallbackSymbols = (node: ts.Node): void => {
250
+ if (isTopLevelCallback(node) && containsSupportedOutboundCall(node)) {
251
+ const startLine = lineOf(source, node.getStart(source));
252
+ const name = `callback:${startLine}`;
253
+ symbols.push({ kind: 'callback', localName: name, qualifiedName: `module:${sourceFile}#${name}`, sourceFile, startLine, endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: false, importExportEvidence: { source: 'synthetic_outbound_callback', callbackLine: startLine } });
254
+ }
255
+ ts.forEachChild(node, visitCallbackSymbols);
256
+ };
257
+ visitCallbackSymbols(source);
258
+
259
+ const visitEventRegistrationSymbols = (node: ts.Node): void => {
260
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'on') {
261
+ const receiver = node.expression.expression.getText(source);
262
+ const eventArg = node.arguments[0];
263
+ if ((receiver === 'cds' || /^(srv|service|serviceClient|messaging|messageClient|eventClient|.*Client)$/.test(receiver)) && eventArg && (ts.isStringLiteral(eventArg) || ts.isNoSubstitutionTemplateLiteral(eventArg))) {
264
+ const startLine = lineOf(source, node.getStart(source));
265
+ const eventName = eventArg.text.replace(/[^A-Za-z0-9_$-]/g, '_');
266
+ const name = `event:${eventName}:${startLine}`;
267
+ symbols.push({ kind: 'event_registration', localName: name, qualifiedName: `module:${sourceFile}#${name}`, sourceFile, startLine, endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: false, importExportEvidence: { source: 'synthetic_event_registration', eventName: eventArg.text, registrationLine: startLine, receiver } });
268
+ }
269
+ }
270
+ ts.forEachChild(node, visitEventRegistrationSymbols);
271
+ };
272
+ visitEventRegistrationSymbols(source);
273
+ const visitProxyVariables = (node: ts.Node): void => {
274
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isPropertyAccessExpression(node.initializer.expression)) {
275
+ const callee = callName(node.initializer.expression);
276
+ const importSource = callee.local ? imports.get(callee.local) : undefined;
277
+ if (callee.member && importSource && isRelativeImport(importSource)) proxyVariables.set(node.name.text, { importSource, factory: callee.expression, variableName: node.name.text });
278
+ }
279
+ ts.forEachChild(node, visitProxyVariables);
280
+ };
281
+ visitProxyVariables(source);
282
+ const rememberClassInstance = (variableName: string, className: string, propertyName?: string): void => {
283
+ const importSource = imports.get(className);
284
+ if (!builtInConstructors.has(className) && ((importSource && isRelativeImport(importSource)) || declaredClasses.has(className))) classInstances.set(variableName, { className, importSource, propertyName });
285
+ };
286
+ const visitClassInstances = (node: ts.Node): void => {
287
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isNewExpression(node.initializer) && ts.isIdentifier(node.initializer.expression)) {
288
+ rememberClassInstance(node.name.text, node.initializer.expression.text);
289
+ }
290
+ if (ts.isPropertyDeclaration(node) && node.initializer && ts.isNewExpression(node.initializer) && ts.isIdentifier(node.initializer.expression)) {
291
+ const propertyName = nameOf(node.name);
292
+ if (propertyName) rememberClassInstance(`this.${propertyName}`, node.initializer.expression.text, propertyName);
293
+ }
294
+ ts.forEachChild(node, visitClassInstances);
295
+ };
296
+ visitClassInstances(source);
297
+ const localCallables = new Set(symbols.flatMap((sym) => [sym.localName, sym.qualifiedName]));
298
+ const visitCalls = (node: ts.Node): void => {
299
+ if (ts.isCallExpression(node)) {
300
+ const line = lineOf(source, node.getStart(source));
301
+ const caller = nearest(symbols, line);
302
+ if (caller) {
303
+ const callee = callName(node.expression);
304
+ const proxy = callee.local ? proxyVariables.get(callee.local) : undefined;
305
+ const instance = (callee.local ? classInstances.get(callee.local) : undefined) ?? (callee.receiver ? classInstances.get(callee.receiver) : undefined);
306
+ const importSource = instance?.importSource ?? proxy?.importSource ?? (callee.local ? imports.get(callee.local) : undefined) ?? (callee.member && callee.local ? imports.get(callee.local) : undefined);
307
+ const directThisMethod = callee.receiver === 'this';
308
+ const targetName = instance && callee.member ? `${instance.className}.${callee.member}` : proxy && callee.member ? callee.member : directThisMethod ? callee.member : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
309
+ const className = caller.qualifiedName.includes('.') ? caller.qualifiedName.split('.')[0] : undefined;
310
+ const thisTarget = directThisMethod && className && callee.member ? `${className}.${callee.member}` : undefined;
311
+ const loggerLike = callee.receiver?.endsWith('.logger') || callee.local === 'logger' || (callee.expression.startsWith('this.logger.') && callee.member ? loggerMembers.has(callee.member) : false);
312
+ const terminalMember = callee.member ? commonTerminalMembers.has(callee.member) || loggerMembers.has(callee.member) : false;
313
+ const provenLocal = Boolean(targetName) && localCallables.has(String(targetName));
314
+ const provenThisMethod = Boolean(thisTarget && localCallables.has(thisTarget));
315
+ const provenRelativeImport = Boolean(isRelativeImport(importSource) && targetName);
316
+ const provenClassInstance = Boolean(instance && callee.member && targetName);
317
+ const importedFromPackage = Boolean(importSource && !isRelativeImport(importSource));
318
+ const ignored = loggerLike || terminalMember || importedFromPackage || ignoredFrameworkCall(callee);
319
+ const resolvedTarget = provenThisMethod ? thisTarget : targetName;
320
+ const keep = Boolean(resolvedTarget) && !ignored && (provenLocal || provenThisMethod || provenRelativeImport || provenClassInstance);
321
+ if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: resolvedTarget, receiverLocalName: callee.member ? (callee.local ?? callee.receiver) : undefined, importSource, sourceFile, sourceLine: line, evidence: { relation: instance ? 'class_instance_method' : proxy ? 'relative_import_proxy_member' : importSource ? 'relative_import' : provenThisMethod ? 'indexed_this_method' : 'indexed_local_symbol', caller: caller.qualifiedName, targetName: resolvedTarget, instanceVariable: instance ? (instance.propertyName ?? callee.local) : undefined, className: instance?.className, methodName: instance ? callee.member : undefined, classImportSource: instance?.importSource, callArguments: argumentEvidence(node.arguments, source), proxyVariableName: proxy?.variableName, factory: proxy?.factory, factoryExpression: proxy?.factory, factoryImportSource: proxy?.importSource, candidateStrategy: instance ? (instance.importSource ? 'relative_import_class_instance_method' : 'same_file_class_instance_method') : proxy ? 'proxy_member_exact_export_or_unique_member' : undefined } });
322
+ }
323
+ }
324
+ ts.forEachChild(node, visitCalls);
325
+ };
326
+ visitCalls(source);
327
+ return { symbols, calls };
328
+ }
@@ -0,0 +1,13 @@
1
+ import ts from 'typescript';
2
+ export function createSourceFile(
3
+ filePath: string,
4
+ text: string
5
+ ): ts.SourceFile {
6
+ return ts.createSourceFile(
7
+ filePath,
8
+ text,
9
+ ts.ScriptTarget.Latest,
10
+ true,
11
+ filePath.endsWith('.js') ? ts.ScriptKind.JS : ts.ScriptKind.TS
12
+ );
13
+ }
@@ -0,0 +1,20 @@
1
+ import type { TraceStart } from '../types.js';
2
+ export function parseVars(
3
+ values: string[] | undefined
4
+ ): Record<string, string> {
5
+ const out: Record<string, string> = {};
6
+ for (const value of values ?? []) {
7
+ const [key, ...rest] = value.split('=');
8
+ if (key && rest.length > 0) out[key] = rest.join('=');
9
+ }
10
+ return out;
11
+ }
12
+ export function startLabel(start: TraceStart): string {
13
+ return [
14
+ start.repo,
15
+ start.servicePath,
16
+ start.operation ?? start.operationPath ?? start.handler
17
+ ]
18
+ .filter(Boolean)
19
+ .join(' ');
20
+ }