@saptools/service-flow 0.1.67 → 0.1.68

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 (80) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +20 -8
  3. package/TECHNICAL-NOTE.md +27 -0
  4. package/dist/{chunk-ZQABU7MR.js → chunk-AEM4JY22.js} +12676 -6714
  5. package/dist/chunk-AEM4JY22.js.map +1 -0
  6. package/dist/cli.js +2325 -413
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +61 -2
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/003-doctor-package-resolution.ts +68 -0
  12. package/src/cli/doctor.ts +25 -14
  13. package/src/db/000-call-fact-repository.ts +475 -342
  14. package/src/db/001-fact-lifecycle.ts +60 -30
  15. package/src/db/002-fact-json-inventory.ts +169 -0
  16. package/src/db/003-current-fact-semantics.ts +698 -0
  17. package/src/db/004-package-target-invalidation.ts +173 -0
  18. package/src/db/005-schema-structure.ts +201 -0
  19. package/src/db/006-relative-symbol-resolution.ts +443 -0
  20. package/src/db/007-package-fact-semantics.ts +573 -0
  21. package/src/db/008-relative-fact-semantics.ts +207 -0
  22. package/src/db/009-binding-fact-semantics.ts +347 -0
  23. package/src/db/010-package-symbol-surface-semantics.ts +320 -0
  24. package/src/db/011-symbol-call-semantics.ts +144 -0
  25. package/src/db/012-binding-reference-proof.ts +264 -0
  26. package/src/db/migrations.ts +16 -3
  27. package/src/db/repositories.ts +113 -6
  28. package/src/db/schema.ts +4 -2
  29. package/src/index.ts +12 -0
  30. package/src/indexer/repository-indexer.ts +72 -11
  31. package/src/indexer/workspace-indexer.ts +123 -32
  32. package/src/linker/003-package-import-symbol-resolver.ts +363 -131
  33. package/src/linker/004-event-subscription-handler-linker.ts +2 -0
  34. package/src/linker/005-odata-path-structure.ts +371 -0
  35. package/src/linker/cross-repo-linker.ts +2 -1
  36. package/src/linker/odata-path-normalizer.ts +273 -180
  37. package/src/linker/service-resolver.ts +197 -77
  38. package/src/parsers/002-symbol-import-bindings.ts +516 -0
  39. package/src/parsers/003-package-public-surface.ts +661 -0
  40. package/src/parsers/004-fact-identity.ts +108 -0
  41. package/src/parsers/005-event-subscription-facts.ts +281 -0
  42. package/src/parsers/006-binding-identity.ts +343 -0
  43. package/src/parsers/007-source-fact-reconciliation.ts +105 -0
  44. package/src/parsers/008-package-surface-publication.ts +82 -0
  45. package/src/parsers/009-symbol-call-facts.ts +528 -0
  46. package/src/parsers/010-package-public-surface-analysis.ts +352 -0
  47. package/src/parsers/011-binding-lexical-scope.ts +583 -0
  48. package/src/parsers/012-package-fact-contract.ts +306 -0
  49. package/src/parsers/013-executable-body-eligibility.ts +35 -0
  50. package/src/parsers/014-service-binding-helper-flow.ts +240 -0
  51. package/src/parsers/015-service-binding-collector.ts +693 -0
  52. package/src/parsers/016-local-symbol-reference.ts +261 -0
  53. package/src/parsers/017-symbol-derived-contexts.ts +268 -0
  54. package/src/parsers/018-package-commonjs-syntax.ts +142 -0
  55. package/src/parsers/019-binding-assignment-targets.ts +76 -0
  56. package/src/parsers/020-stable-local-value.ts +217 -0
  57. package/src/parsers/021-binding-visibility.ts +152 -0
  58. package/src/parsers/operation-path-analysis.ts +6 -1
  59. package/src/parsers/outbound-call-parser.ts +19 -6
  60. package/src/parsers/package-json-parser.ts +45 -3
  61. package/src/parsers/service-binding-parser-helpers.ts +86 -15
  62. package/src/parsers/service-binding-parser.ts +147 -597
  63. package/src/parsers/symbol-parser.ts +482 -353
  64. package/src/trace/002-trace-diagnostics.ts +36 -1
  65. package/src/trace/016-compact-projector.ts +16 -17
  66. package/src/trace/020-compact-field-projection.ts +48 -37
  67. package/src/trace/021-compact-decision-normalization.ts +105 -0
  68. package/src/trace/022-trace-fact-preflight.ts +21 -0
  69. package/src/trace/023-nested-event-scopes.ts +23 -0
  70. package/src/trace/024-compact-observation-decision.ts +76 -0
  71. package/src/trace/025-trace-implementation-scope.ts +123 -0
  72. package/src/trace/026-trace-start-scope.ts +335 -0
  73. package/src/trace/027-trace-scope-execution.ts +566 -0
  74. package/src/trace/028-trace-operation-execution.ts +336 -0
  75. package/src/trace/029-trace-start-implementation.ts +172 -0
  76. package/src/trace/trace-engine.ts +122 -624
  77. package/src/types.ts +56 -0
  78. package/src/utils/001-placeholders.ts +188 -10
  79. package/src/version.ts +1 -1
  80. package/dist/chunk-ZQABU7MR.js.map +0 -1
@@ -5,9 +5,28 @@ import type { ExecutableSymbolFact, SymbolCallFact } from '../types.js';
5
5
  import {
6
6
  classifyOutboundCallsInSource,
7
7
  containsSupportedOutboundCall,
8
+ type ClassifiedOutboundCall,
8
9
  } from './outbound-call-parser.js';
9
10
  import type { RepositorySourceContext } from './ts-project.js';
10
11
  import { normalizePath } from '../utils/path-utils.js';
12
+ import { reconcileEventSubscriptions } from './005-event-subscription-facts.js';
13
+ import { reconcileSymbolCallOwners } from './007-source-fact-reconciliation.js';
14
+ import {
15
+ collectSymbolImportBindings,
16
+ type SymbolImportBinding,
17
+ } from './002-symbol-import-bindings.js';
18
+ import {
19
+ collectSymbolCallFacts,
20
+ symbolCallName,
21
+ type SymbolCallProxy,
22
+ type SymbolClassInstance,
23
+ } from './009-symbol-call-facts.js';
24
+ import {
25
+ executableBodyEligibility,
26
+ } from './013-executable-body-eligibility.js';
27
+ import {
28
+ collectDerivedSymbolContexts,
29
+ } from './017-symbol-derived-contexts.js';
11
30
 
12
31
  function lineOf(source: ts.SourceFile, pos: number): number {
13
32
  return source.getLineAndCharacterOfPosition(pos).line + 1;
@@ -38,80 +57,9 @@ function exportDeclarations(source: ts.SourceFile): Map<string, string> {
38
57
  visit(source);
39
58
  return exports;
40
59
  }
41
- function isRelativeImport(value: string | undefined): boolean {
42
- return Boolean(value?.startsWith('.'));
43
- }
44
- type HandlerReferenceRelation =
45
- | 'relative_import'
46
- | 'package_import'
47
- | 'indexed_local_symbol'
48
- | 'relative_import_namespace_member';
49
- interface HandlerReferenceTarget {
50
- calleeExpression: string;
51
- calleeLocalName: string;
52
- importSource?: string;
53
- relation: HandlerReferenceRelation;
54
- wrapperFunction?: string;
55
- }
56
- function directHandlerReferenceTarget(
57
- expression: ts.Expression,
58
- source: ts.SourceFile,
59
- imports: Map<string, string>,
60
- namespaceImports: Set<string>,
61
- ): HandlerReferenceTarget | undefined {
62
- if (ts.isIdentifier(expression)) {
63
- const importSource = imports.get(expression.text);
64
- return {
65
- calleeExpression: expression.text,
66
- calleeLocalName: expression.text,
67
- importSource,
68
- relation: importSource
69
- ? isRelativeImport(importSource) ? 'relative_import' : 'package_import'
70
- : 'indexed_local_symbol',
71
- };
72
- }
73
- if (!ts.isPropertyAccessExpression(expression) || expression.questionDotToken
74
- || !ts.isIdentifier(expression.expression) || !ts.isIdentifier(expression.name))
75
- return undefined;
76
- const objectName = expression.expression.text;
77
- const memberName = expression.name.text;
78
- const importSource = imports.get(objectName);
79
- if (namespaceImports.has(objectName)) return {
80
- calleeExpression: expression.getText(source),
81
- calleeLocalName: memberName,
82
- importSource,
83
- relation: 'relative_import_namespace_member',
84
- };
85
- const qualifiedName = `${objectName}.${memberName}`;
86
- return {
87
- calleeExpression: qualifiedName,
88
- calleeLocalName: qualifiedName,
89
- importSource,
90
- relation: importSource
91
- ? isRelativeImport(importSource) ? 'relative_import' : 'package_import'
92
- : 'indexed_local_symbol',
93
- };
94
- }
95
- function handlerReferenceTarget(
96
- expression: ts.Expression,
97
- source: ts.SourceFile,
98
- imports: Map<string, string>,
99
- namespaceImports: Set<string>,
100
- ): HandlerReferenceTarget | undefined {
101
- const direct = directHandlerReferenceTarget(
102
- expression, source, imports, namespaceImports,
103
- );
104
- if (direct) return direct;
105
- if (!ts.isCallExpression(expression) || expression.questionDotToken
106
- || expression.arguments.length !== 1) return undefined;
107
- const inner = directHandlerReferenceTarget(
108
- expression.arguments[0], source, imports, namespaceImports,
109
- );
110
- return inner
111
- ? { ...inner, wrapperFunction: expression.expression.getText(source) }
112
- : undefined;
113
- }
114
- function isObjectFunction(node: ts.Node): boolean {
60
+ function isObjectFunction(
61
+ node: ts.Node,
62
+ ): node is ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration {
115
63
  return ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node);
116
64
  }
117
65
  type ParameterBinding =
@@ -119,69 +67,11 @@ type ParameterBinding =
119
67
  | { index: number; kind: 'object_pattern'; properties: Array<{ property: string; local: string }> }
120
68
  | { index: number; kind: 'array_pattern'; elements: Array<{ index: number; local: string }> };
121
69
  type ParameterPropertyAlias = { parameter: string; property: string; local: string; kind: 'object_parameter_destructure'; line: number };
122
- const commonTerminalMembers = new Set(['push', 'includes', 'find', 'findIndex', 'map', 'filter', 'reduce', 'forEach', 'some', 'every', 'toUpperCase', 'toLowerCase', 'trim', 'split', 'join', 'get', 'set', 'has']);
123
- const loggerMembers = new Set(['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'log']);
124
- const globalObjects = new Set(['JSON', 'Object', 'Array', 'String', 'Number', 'Boolean', 'Math', 'Date', 'Promise', 'Reflect']);
125
- const builtInConstructors = new Set([
126
- 'Set', 'Map', 'WeakSet', 'WeakMap',
127
- 'Date', 'RegExp', 'URL', 'URLSearchParams',
128
- 'Error', 'EvalError', 'RangeError', 'ReferenceError', 'SyntaxError', 'TypeError', 'URIError', 'AggregateError',
129
- 'ArrayBuffer', 'SharedArrayBuffer', 'DataView',
130
- 'Int8Array', 'Uint8Array', 'Uint8ClampedArray', 'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array',
131
- 'Float32Array', 'Float64Array', 'BigInt64Array', 'BigUint64Array',
132
- 'Promise', 'AbortController',
133
- ]);
134
- const capDslRoots = new Set(['SELECT', 'INSERT', 'UPSERT', 'UPDATE', 'DELETE']);
135
- const requestHelpers = new Set(['reject', 'error', 'info', 'warn', 'notify']);
136
- const transportMembers = new Set(['emit', 'publish', 'send', 'on']);
137
- function callName(expr: ts.Expression): { expression: string; local?: string; member?: string; receiver?: string } {
138
- if (ts.isIdentifier(expr)) return { expression: expr.text, local: expr.text };
139
- if (ts.isPropertyAccessExpression(expr)) {
140
- const left = expr.expression.getText();
141
- const root = left.split('.')[0];
142
- return { expression: expr.getText(), local: left === 'this' ? undefined : root, member: expr.name.text, receiver: left };
143
- }
144
- return { expression: expr.getText() };
145
- }
146
70
  function requireSource(expr: ts.Expression): string | undefined {
147
71
  if (!ts.isCallExpression(expr) || !ts.isIdentifier(expr.expression) || expr.expression.text !== 'require') return undefined;
148
72
  const first = expr.arguments[0];
149
73
  return first && ts.isStringLiteral(first) ? first.text : undefined;
150
74
  }
151
- function ignoredFrameworkCall(callee: { expression: string; local?: string; member?: string; receiver?: string }): boolean {
152
- if (callee.local && capDslRoots.has(callee.local)) return true;
153
- if (callee.expression === 'cds.run' || callee.expression.startsWith('cds.connect.') || callee.expression.startsWith('cds.services.') || callee.expression.startsWith('cds.parse.')) return true;
154
- if (callee.local === 'req' && callee.member && requestHelpers.has(callee.member)) return true;
155
- if (callee.member && transportMembers.has(callee.member)) return true;
156
- if (callee.local && globalObjects.has(callee.local)) return true;
157
- if (callee.expression.startsWith('new Date().')) return true;
158
- return false;
159
- }
160
- function nearest(symbols: ExecutableSymbolFact[], line: number): ExecutableSymbolFact | undefined {
161
- return symbols.filter((s) => s.startLine <= line && s.endLine >= line).sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
162
- }
163
- function argumentEvidence(args: ts.NodeArray<ts.Expression>, source: ts.SourceFile): Array<Record<string, unknown>> {
164
- return args.map((arg) => {
165
- if (ts.isIdentifier(arg)) return { kind: 'identifier', name: arg.text };
166
- if (ts.isObjectLiteralExpression(arg)) {
167
- const properties: Array<Record<string, unknown>> = [];
168
- for (const prop of arg.properties) {
169
- if (ts.isShorthandPropertyAssignment(prop)) properties.push({ kind: 'shorthand', property: prop.name.text, argument: prop.name.text });
170
- if (ts.isPropertyAssignment(prop)) {
171
- const propName = nameOf(prop.name);
172
- if (propName && ts.isIdentifier(prop.initializer)) properties.push({ kind: 'property_assignment', property: propName, argument: prop.initializer.text });
173
- }
174
- }
175
- return { kind: 'object_literal', properties };
176
- }
177
- if (ts.isArrayLiteralExpression(arg)) {
178
- const elements = arg.elements.flatMap((element, index): Array<Record<string, unknown>> =>
179
- ts.isIdentifier(element) ? [{ index, kind: 'identifier', name: element.text }] : []);
180
- return { kind: 'array_literal', elements };
181
- }
182
- return { kind: 'unsupported', expression: arg.getText(source) };
183
- });
184
- }
185
75
  function bindingLocalName(name: ts.BindingName, initializer?: ts.Expression): string | undefined {
186
76
  if (ts.isIdentifier(name)) return name.text;
187
77
  if (initializer && ts.isIdentifier(initializer)) return initializer.text;
@@ -241,237 +131,476 @@ function parameterBindings(params: ts.NodeArray<ts.ParameterDeclaration>): Param
241
131
  return [];
242
132
  });
243
133
  }
244
- export async function parseExecutableSymbols(
245
- repoPath: string,
246
- filePath: string,
247
- context?: RepositorySourceContext,
248
- ): Promise<{ symbols: ExecutableSymbolFact[]; calls: SymbolCallFact[] }> {
249
- const snapshot = context?.get(filePath);
250
- const text = snapshot?.text
251
- ?? await fs.readFile(path.join(repoPath, filePath), 'utf8');
252
- const source = snapshot?.sourceFile() ?? ts.createSourceFile(
253
- filePath, text, ts.ScriptTarget.Latest, true,
254
- filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS,
255
- );
256
- const sourceFile = normalizePath(filePath);
257
- const symbols: ExecutableSymbolFact[] = [];
258
- const calls: SymbolCallFact[] = [];
259
- const imports = new Map<string, string>();
260
- const namespaceImports = new Set<string>();
261
- const eventSubscriptionOffsets = new Set(
262
- classifyOutboundCallsInSource(source, sourceFile)
263
- .filter((call) => call.fact.callType === 'async_subscribe')
264
- .map((call) => `${call.node.getStart(source)}:${call.node.getEnd()}`),
265
- );
266
- const exportNames = exportDeclarations(source);
267
- const objectExports = new Set<string>();
268
- const exportedClasses = new Set<string>();
269
- const declaredClasses = new Set<string>();
270
- const proxyVariables = new Map<string, { importSource: string; factory: string; variableName: string }>();
271
- const classInstances = new Map<string, { className: string; importSource?: string; propertyName?: string }>();
272
- const addSymbol = (kind: string, localName: string, node: ts.Node, parentName?: string, exportedName?: string, evidence?: Record<string, unknown>): void => {
273
- const parentRoot = parentName?.split('.')[0] ?? '';
274
- const declaredExportName = exportedName ?? exportNames.get(parentName ? parentRoot : localName);
275
- const qualifiedName = parentName ? `${parentName}.${localName}` : localName;
276
- const objectExported = parentName ? objectExports.has(parentRoot) : false;
277
- const classMemberExported = kind === 'method' && parentName ? exportedClasses.has(parentRoot) && ts.isMethodDeclaration(node) && (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Static) !== 0 && isPublicClassMethod(node) : false;
278
- const effectiveExportedName = classMemberExported || objectExported ? qualifiedName : declaredExportName;
279
- const bindings = isFunctionLike(node) ? parameterBindings(node.parameters) : undefined;
280
- const params = bindings?.flatMap((binding) => binding.kind === 'identifier' ? [binding.name] : []);
281
- 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);
282
- const aliases = isFunctionLike(node) ? parameterPropertyAliases(node, source) : [];
283
- const parameterEvidence = { ...(bindings && bindings.length > 0 ? { parameters: params, parameterBindings: bindings } : {}), ...(aliases.length > 0 ? { parameterPropertyAliases: aliases } : {}) };
284
- 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 });
285
- };
286
- const addAliasSymbol = (objectName: string, propertyName: string, node: ts.Node, targetImportSource?: string): void => {
287
- 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 } });
134
+ interface SymbolCollection {
135
+ source: ts.SourceFile;
136
+ sourceFile: string;
137
+ symbols: ExecutableSymbolFact[];
138
+ imports: Map<string, string>;
139
+ importBindings: SymbolImportBinding[];
140
+ classifiedCalls: readonly ClassifiedOutboundCall[];
141
+ exportNames: Map<string, string>;
142
+ objectExports: Set<string>;
143
+ exportedClasses: Set<string>;
144
+ declaredClasses: Set<string>;
145
+ proxies: Map<string, SymbolCallProxy[]>;
146
+ instances: Map<string, SymbolClassInstance[]>;
147
+ }
148
+
149
+ function symbolSourceEvidence(
150
+ collection: SymbolCollection,
151
+ node: ts.Node,
152
+ options: {
153
+ parentRoot: string;
154
+ qualifiedName: string;
155
+ declaredExportName?: string;
156
+ classContainerExported: boolean; classMemberExported: boolean;
157
+ objectExported: boolean;
158
+ evidence?: Record<string, unknown>;
159
+ },
160
+ ): Record<string, unknown> {
161
+ if (options.evidence) return options.evidence;
162
+ if (options.classMemberExported) return {
163
+ source: 'exported_class_member',
164
+ exportedClass: options.parentRoot,
165
+ memberKind: ts.getCombinedModifierFlags(node as ts.Declaration)
166
+ & ts.ModifierFlags.Static ? 'static_method' : 'class_method',
288
167
  };
289
- const visitImports = (node: ts.Node): void => {
290
- if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
291
- const sourceText = node.moduleSpecifier.text;
292
- const clause = node.importClause;
293
- if (clause?.name) imports.set(clause.name.text, sourceText);
294
- const named = clause?.namedBindings;
295
- if (named && ts.isNamedImports(named)) for (const el of named.elements) imports.set(el.name.text, sourceText);
296
- if (named && ts.isNamespaceImport(named)) {
297
- imports.set(named.name.text, sourceText);
298
- namespaceImports.add(named.name.text);
299
- }
300
- }
301
- if (ts.isVariableStatement(node)) {
302
- for (const declaration of node.declarationList.declarations) {
303
- const requiredSource = declaration.initializer ? requireSource(declaration.initializer) : undefined;
304
- if (ts.isIdentifier(declaration.name) && requiredSource) imports.set(declaration.name.text, requiredSource);
305
- if (ts.isObjectBindingPattern(declaration.name) && requiredSource) for (const element of declaration.name.elements) if (ts.isIdentifier(element.name)) imports.set(element.name.text, requiredSource);
306
- }
307
- }
308
- ts.forEachChild(node, visitImports);
168
+ if (options.classContainerExported && ts.isMethodDeclaration(node)
169
+ && isPublicClassMethod(node)) return {
170
+ source: 'exported_class_instance_member', exportedClass: options.parentRoot,
171
+ memberKind: 'class_method',
309
172
  };
310
- visitImports(source);
311
- const visitSymbols = (node: ts.Node, parentClass?: string): void => {
312
- if (ts.isClassDeclaration(node) && node.name) {
313
- declaredClasses.add(node.name.text);
314
- if (exported(node) || exportNames.has(node.name.text)) exportedClasses.add(node.name.text);
315
- for (const member of node.members) visitSymbols(member, node.name.text);
316
- return;
317
- }
318
- if (ts.isMethodDeclaration(node)) {
319
- const localName = nameOf(node.name);
320
- if (localName) addSymbol('method', localName, node, parentClass);
321
- } else if (ts.isPropertyDeclaration(node) && parentClass && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
322
- const localName = nameOf(node.name);
323
- if (localName) {
324
- const flags = ts.getCombinedModifierFlags(node);
325
- const staticPublicExported = exportedClasses.has(parentClass) && (flags & ts.ModifierFlags.Static) !== 0 && (flags & ts.ModifierFlags.Private) === 0 && (flags & ts.ModifierFlags.Protected) === 0;
326
- const isArrow = ts.isArrowFunction(node.initializer);
327
- const memberKind = isArrow
328
- ? (staticPublicExported ? 'static_arrow_function' : 'arrow_function_property')
329
- : (staticPublicExported ? 'static_function_expression' : 'function_expression_property');
330
- addSymbol('method', localName, node.initializer, parentClass, staticPublicExported ? `${parentClass}.${localName}` : undefined, staticPublicExported
331
- ? { source: 'exported_class_member', exportedClass: parentClass, memberKind }
332
- : { source: 'class_property_function', memberKind });
333
- }
334
- } else if (ts.isFunctionDeclaration(node) && node.name) addSymbol('function', node.name.text, node, undefined, exported(node) ? node.name.text : undefined);
335
- else if (ts.isVariableStatement(node)) {
336
- for (const d of node.declarationList.declarations) {
337
- const localName = nameOf(d.name);
338
- if (!localName || !d.initializer) continue;
339
- if (isFunctionLike(d.initializer)) addSymbol('function', localName, d.initializer, undefined, exported(node) ? localName : exportNames.get(localName));
340
- if (ts.isObjectLiteralExpression(d.initializer)) {
341
- const objectIsExported = exported(node) || exportNames.has(localName);
342
- if (objectIsExported) objectExports.add(localName);
343
- for (const prop of d.initializer.properties) {
344
- if (objectIsExported && ts.isShorthandPropertyAssignment(prop)) addAliasSymbol(localName, prop.name.text, prop.name, imports.get(prop.name.text));
345
- if (ts.isPropertyAssignment(prop) && isObjectFunction(prop.initializer)) {
346
- const propName = nameOf(prop.name);
347
- if (propName) addSymbol('object_method', propName, prop.initializer, localName);
348
- } else if (ts.isMethodDeclaration(prop)) {
349
- const propName = nameOf(prop.name);
350
- if (propName) addSymbol('object_method', propName, prop, localName);
351
- }
352
- }
353
- }
354
- }
355
- } else ts.forEachChild(node, (child) => visitSymbols(child, parentClass));
173
+ if (options.declaredExportName) return {
174
+ exportedName: options.declaredExportName,
175
+ source: 'export_declaration',
356
176
  };
357
- visitSymbols(source);
177
+ return options.objectExported
178
+ ? { exportedName: options.qualifiedName, source: 'exported_object_literal' }
179
+ : {};
180
+ }
358
181
 
359
- const isTopLevelCallback = (node: ts.Node): node is ts.ArrowFunction | ts.FunctionExpression => {
360
- if (!ts.isArrowFunction(node) && !ts.isFunctionExpression(node)) return false;
361
- if (!ts.isCallExpression(node.parent)) return false;
362
- const callee = callName(node.parent.expression);
363
- const member = callee.member ?? callee.local;
364
- return Boolean(member && ['bootstrap', 'served', 'connect', 'on', 'once', 'use', 'get', 'post', 'put', 'patch', 'delete', 'subscribe'].includes(member));
182
+ function executableEvidence(
183
+ node: ts.Node,
184
+ source: ts.SourceFile,
185
+ ): Record<string, unknown> {
186
+ if (!isFunctionLike(node)) return {};
187
+ const bindings = parameterBindings(node.parameters);
188
+ const parameters = bindings.flatMap((binding) =>
189
+ binding.kind === 'identifier' ? [binding.name] : []);
190
+ const aliases = parameterPropertyAliases(node, source);
191
+ return {
192
+ executableBodyEligibility: executableBodyEligibility(node, source),
193
+ ...(bindings.length > 0 ? { parameters, parameterBindings: bindings } : {}),
194
+ ...(aliases.length > 0 ? { parameterPropertyAliases: aliases } : {}),
365
195
  };
366
- const visitCallbackSymbols = (node: ts.Node): void => {
367
- if (isTopLevelCallback(node) && containsSupportedOutboundCall(node)) {
368
- const startLine = lineOf(source, node.getStart(source));
369
- const name = `callback:${startLine}`;
370
- 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 } });
371
- }
372
- ts.forEachChild(node, visitCallbackSymbols);
196
+ }
197
+
198
+ interface SymbolNames {
199
+ parentRoot: string;
200
+ qualifiedName: string;
201
+ declaredExportName?: string;
202
+ objectExported: boolean;
203
+ classContainerExported: boolean;
204
+ classMemberExported: boolean;
205
+ effectiveName?: string;
206
+ }
207
+
208
+ function exportedClassMember(
209
+ collection: SymbolCollection,
210
+ kind: string,
211
+ parentName: string | undefined,
212
+ parentRoot: string,
213
+ node: ts.Node,
214
+ ): boolean {
215
+ if (kind !== 'method' || !parentName
216
+ || !collection.exportedClasses.has(parentRoot)
217
+ || !ts.isMethodDeclaration(node)) return false;
218
+ return Boolean(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Static)
219
+ && isPublicClassMethod(node);
220
+ }
221
+
222
+ function symbolNames(
223
+ collection: SymbolCollection,
224
+ kind: string,
225
+ localName: string,
226
+ node: ts.Node,
227
+ parentName?: string,
228
+ exportedName?: string,
229
+ ): SymbolNames {
230
+ const parentRoot = parentName?.split('.')[0] ?? '';
231
+ const declaredExportName = exportedName ?? collection.exportNames.get(
232
+ parentName ? parentRoot : localName,
233
+ );
234
+ const qualifiedName = parentName ? `${parentName}.${localName}` : localName;
235
+ const objectExported = Boolean(
236
+ parentName && collection.objectExports.has(parentRoot),
237
+ );
238
+ const classMemberExported = exportedClassMember(
239
+ collection, kind, parentName, parentRoot, node,
240
+ );
241
+ const classContainerExported = Boolean(
242
+ parentName && collection.exportedClasses.has(parentRoot),
243
+ );
244
+ return {
245
+ parentRoot, declaredExportName, qualifiedName,
246
+ objectExported, classContainerExported, classMemberExported,
247
+ effectiveName: classMemberExported || objectExported
248
+ ? qualifiedName : declaredExportName,
373
249
  };
374
- visitCallbackSymbols(source);
250
+ }
375
251
 
376
- const visitEventRegistrationSymbols = (node: ts.Node): void => {
377
- if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'on') {
378
- const receiver = node.expression.expression.getText(source);
379
- const eventArg = node.arguments[0];
380
- if ((receiver === 'cds' || /^(srv|service|serviceClient|messaging|messageClient|eventClient|.*Client)$/.test(receiver)) && eventArg && (ts.isStringLiteral(eventArg) || ts.isNoSubstitutionTemplateLiteral(eventArg))) {
381
- const startLine = lineOf(source, node.getStart(source));
382
- const eventName = eventArg.text.replace(/[^A-Za-z0-9_$-]/g, '_');
383
- const name = `event:${eventName}:${startLine}`;
384
- 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 } });
385
- }
386
- if (eventSubscriptionOffsets.has(`${node.getStart(source)}:${node.getEnd()}`)) {
387
- const startLine = lineOf(source, node.getStart(source));
388
- const handlerArgument = node.arguments[1];
389
- const target = handlerArgument
390
- ? handlerReferenceTarget(
391
- handlerArgument, source, imports, namespaceImports,
392
- )
393
- : undefined;
394
- const anchor = nearest(symbols, startLine);
395
- if (target && anchor) calls.push({
396
- callerQualifiedName: anchor.qualifiedName,
397
- calleeExpression: target.calleeExpression,
398
- calleeLocalName: target.calleeLocalName,
399
- importSource: target.importSource,
400
- sourceFile,
401
- sourceLine: startLine,
402
- callSiteStartOffset: node.getStart(source),
403
- callSiteEndOffset: node.getEnd(),
404
- callRole: 'event_subscribe_handler',
405
- evidence: {
406
- relation: target.relation,
407
- caller: anchor.qualifiedName,
408
- targetName: target.calleeLocalName,
409
- ...(target.wrapperFunction
410
- ? { wrapperFunction: target.wrapperFunction }
411
- : {}),
412
- factOrigin: 'event_subscribe_handler_reference',
413
- },
414
- });
415
- }
416
- }
417
- ts.forEachChild(node, visitEventRegistrationSymbols);
252
+ function addExecutableSymbol(
253
+ collection: SymbolCollection,
254
+ kind: string,
255
+ localName: string,
256
+ node: ts.Node,
257
+ parentName?: string,
258
+ exportedName?: string,
259
+ evidence?: Record<string, unknown>,
260
+ ): void {
261
+ const names = symbolNames(
262
+ collection, kind, localName, node, parentName, exportedName,
263
+ );
264
+ const sourceEvidence = symbolSourceEvidence(collection, node, {
265
+ parentRoot: names.parentRoot,
266
+ qualifiedName: names.qualifiedName,
267
+ declaredExportName: names.declaredExportName,
268
+ classContainerExported: names.classContainerExported,
269
+ classMemberExported: names.classMemberExported,
270
+ objectExported: names.objectExported,
271
+ evidence,
272
+ });
273
+ collection.symbols.push({
274
+ kind,
275
+ localName: kind === 'object_method' ? names.qualifiedName : localName,
276
+ exportedName: names.effectiveName,
277
+ qualifiedName: names.qualifiedName,
278
+ sourceFile: collection.sourceFile,
279
+ startLine: lineOf(collection.source, node.getStart(collection.source)),
280
+ endLine: lineOf(collection.source, node.getEnd()),
281
+ startOffset: node.getStart(collection.source),
282
+ endOffset: node.getEnd(),
283
+ exported: exported(node) || Boolean(names.effectiveName),
284
+ importExportEvidence: {
285
+ ...sourceEvidence,
286
+ ...executableEvidence(node, collection.source),
287
+ },
288
+ });
289
+ }
290
+
291
+ function addAliasSymbol(
292
+ collection: SymbolCollection,
293
+ objectName: string,
294
+ propertyName: string,
295
+ node: ts.Node,
296
+ ): void {
297
+ collection.symbols.push({
298
+ kind: 'object_alias',
299
+ localName: propertyName,
300
+ exportedName: propertyName,
301
+ qualifiedName: `${objectName}.${propertyName}`,
302
+ sourceFile: collection.sourceFile,
303
+ startLine: lineOf(collection.source, node.getStart(collection.source)),
304
+ endLine: lineOf(collection.source, node.getEnd()),
305
+ startOffset: node.getStart(collection.source),
306
+ endOffset: node.getEnd(),
307
+ exported: true,
308
+ importExportEvidence: {
309
+ source: 'exported_object_shorthand',
310
+ objectName,
311
+ propertyName,
312
+ targetImportSource: collection.imports.get(propertyName),
313
+ },
314
+ });
315
+ }
316
+
317
+ function collectImportSources(collection: SymbolCollection): void {
318
+ const visit = (node: ts.Node): void => {
319
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier))
320
+ collectEsmImportSources(collection.imports, node);
321
+ if (ts.isVariableStatement(node))
322
+ collectCjsImportSources(collection.imports, node);
323
+ ts.forEachChild(node, visit);
418
324
  };
419
- visitEventRegistrationSymbols(source);
420
- const visitProxyVariables = (node: ts.Node): void => {
421
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isPropertyAccessExpression(node.initializer.expression)) {
422
- const callee = callName(node.initializer.expression);
423
- const importSource = callee.local ? imports.get(callee.local) : undefined;
424
- if (callee.member && importSource && isRelativeImport(importSource)) proxyVariables.set(node.name.text, { importSource, factory: callee.expression, variableName: node.name.text });
325
+ visit(collection.source);
326
+ }
327
+
328
+ function collectEsmImportSources(
329
+ imports: Map<string, string>,
330
+ node: ts.ImportDeclaration,
331
+ ): void {
332
+ if (!ts.isStringLiteral(node.moduleSpecifier)) return;
333
+ const source = node.moduleSpecifier.text;
334
+ const clause = node.importClause;
335
+ if (clause?.name) imports.set(clause.name.text, source);
336
+ const named = clause?.namedBindings;
337
+ if (named && ts.isNamedImports(named))
338
+ for (const item of named.elements) imports.set(item.name.text, source);
339
+ if (named && ts.isNamespaceImport(named))
340
+ imports.set(named.name.text, source);
341
+ }
342
+
343
+ function collectCjsImportSources(
344
+ imports: Map<string, string>,
345
+ node: ts.VariableStatement,
346
+ ): void {
347
+ for (const declaration of node.declarationList.declarations) {
348
+ const source = declaration.initializer
349
+ ? requireSource(declaration.initializer) : undefined;
350
+ if (!source) continue;
351
+ if (ts.isIdentifier(declaration.name))
352
+ imports.set(declaration.name.text, source);
353
+ if (ts.isObjectBindingPattern(declaration.name))
354
+ for (const item of declaration.name.elements)
355
+ if (ts.isIdentifier(item.name)) imports.set(item.name.text, source);
356
+ }
357
+ }
358
+
359
+ function classPropertySymbol(
360
+ collection: SymbolCollection,
361
+ node: ts.PropertyDeclaration,
362
+ parentClass: string,
363
+ ): void {
364
+ const initializer = node.initializer;
365
+ const localName = nameOf(node.name);
366
+ if (!localName || !initializer
367
+ || (!ts.isArrowFunction(initializer)
368
+ && !ts.isFunctionExpression(initializer))) return;
369
+ const staticPublic = publicStaticProperty(collection, node, parentClass);
370
+ const memberKind = propertyMemberKind(initializer, staticPublic);
371
+ addExecutableSymbol(
372
+ collection, 'method', localName, initializer, parentClass,
373
+ staticPublic ? `${parentClass}.${localName}` : undefined,
374
+ staticPublic
375
+ ? { source: 'exported_class_member', exportedClass: parentClass, memberKind }
376
+ : { source: 'class_property_function', memberKind },
377
+ );
378
+ }
379
+
380
+ function publicStaticProperty(
381
+ collection: SymbolCollection,
382
+ node: ts.PropertyDeclaration,
383
+ parentClass: string,
384
+ ): boolean {
385
+ const flags = ts.getCombinedModifierFlags(node);
386
+ return collection.exportedClasses.has(parentClass)
387
+ && Boolean(flags & ts.ModifierFlags.Static)
388
+ && (flags & ts.ModifierFlags.Private) === 0
389
+ && (flags & ts.ModifierFlags.Protected) === 0;
390
+ }
391
+
392
+ function propertyMemberKind(
393
+ initializer: ts.ArrowFunction | ts.FunctionExpression,
394
+ staticPublic: boolean,
395
+ ): string {
396
+ if (ts.isArrowFunction(initializer))
397
+ return staticPublic ? 'static_arrow_function' : 'arrow_function_property';
398
+ return staticPublic
399
+ ? 'static_function_expression'
400
+ : 'function_expression_property';
401
+ }
402
+
403
+ function objectCallable(
404
+ property: ts.ObjectLiteralElementLike,
405
+ ): ts.FunctionLikeDeclaration | undefined {
406
+ if (ts.isMethodDeclaration(property)) return property;
407
+ return ts.isPropertyAssignment(property)
408
+ && isObjectFunction(property.initializer)
409
+ ? property.initializer
410
+ : undefined;
411
+ }
412
+
413
+ function objectLiteralSymbols(
414
+ collection: SymbolCollection,
415
+ objectName: string,
416
+ object: ts.ObjectLiteralExpression,
417
+ objectIsExported: boolean,
418
+ ): void {
419
+ if (objectIsExported) collection.objectExports.add(objectName);
420
+ for (const property of object.properties) {
421
+ if (objectIsExported && ts.isShorthandPropertyAssignment(property))
422
+ addAliasSymbol(collection, objectName, property.name.text, property.name);
423
+ const callable = objectCallable(property);
424
+ const propertyName = callable ? nameOf(property.name) : undefined;
425
+ if (callable && propertyName)
426
+ addExecutableSymbol(
427
+ collection, 'object_method', propertyName, callable, objectName,
428
+ );
429
+ }
430
+ }
431
+
432
+ function variableSymbols(
433
+ collection: SymbolCollection,
434
+ node: ts.VariableStatement,
435
+ ): void {
436
+ for (const declaration of node.declarationList.declarations) {
437
+ const localName = nameOf(declaration.name);
438
+ const initializer = declaration.initializer;
439
+ if (!localName || !initializer) continue;
440
+ if (isFunctionLike(initializer)) addExecutableSymbol(
441
+ collection, 'function', localName, initializer, undefined,
442
+ exported(node) ? localName : collection.exportNames.get(localName),
443
+ );
444
+ if (ts.isObjectLiteralExpression(initializer))
445
+ objectLiteralSymbols(
446
+ collection, localName, initializer,
447
+ exported(node) || collection.exportNames.has(localName),
448
+ );
449
+ }
450
+ }
451
+
452
+ function collectClassDeclaration(
453
+ collection: SymbolCollection,
454
+ node: ts.Node,
455
+ ): boolean {
456
+ if (!ts.isClassDeclaration(node) || !node.name) return false;
457
+ collection.declaredClasses.add(node.name.text);
458
+ if (exported(node) || collection.exportNames.has(node.name.text))
459
+ collection.exportedClasses.add(node.name.text);
460
+ for (const member of node.members)
461
+ visitDeclaredSymbol(collection, member, node.name.text);
462
+ return true;
463
+ }
464
+
465
+ function collectMethodDeclaration(
466
+ collection: SymbolCollection,
467
+ node: ts.Node,
468
+ parentClass?: string,
469
+ ): boolean {
470
+ if (!ts.isMethodDeclaration(node)) return false;
471
+ const localName = nameOf(node.name);
472
+ if (localName)
473
+ addExecutableSymbol(collection, 'method', localName, node, parentClass);
474
+ return true;
475
+ }
476
+
477
+ function visitDeclaredSymbol(
478
+ collection: SymbolCollection,
479
+ node: ts.Node,
480
+ parentClass?: string,
481
+ ): void {
482
+ if (collectClassDeclaration(collection, node)) return;
483
+ if (collectMethodDeclaration(collection, node, parentClass)) return;
484
+ if (ts.isPropertyDeclaration(node)) {
485
+ if (parentClass) classPropertySymbol(collection, node, parentClass);
486
+ return;
487
+ }
488
+ if (ts.isFunctionDeclaration(node) && node.name) {
489
+ addExecutableSymbol(
490
+ collection, 'function', node.name.text, node, undefined,
491
+ exported(node) ? node.name.text : undefined,
492
+ );
493
+ return;
494
+ }
495
+ if (ts.isVariableStatement(node)) {
496
+ variableSymbols(collection, node);
497
+ return;
498
+ }
499
+ ts.forEachChild(node, (child) =>
500
+ visitDeclaredSymbol(collection, child, parentClass));
501
+ }
502
+
503
+ function collectDeclaredSymbols(collection: SymbolCollection): void {
504
+ visitDeclaredSymbol(collection, collection.source);
505
+ }
506
+
507
+ function isTopLevelCallback(
508
+ node: ts.Node,
509
+ ): node is ts.ArrowFunction | ts.FunctionExpression {
510
+ if ((!ts.isArrowFunction(node) && !ts.isFunctionExpression(node))
511
+ || !ts.isCallExpression(node.parent)) return false;
512
+ const callee = symbolCallName(node.parent.expression);
513
+ const member = callee.member ?? callee.local;
514
+ return Boolean(member && [
515
+ 'bootstrap', 'served', 'connect', 'on', 'once', 'use',
516
+ 'get', 'post', 'put', 'patch', 'delete', 'subscribe',
517
+ ].includes(member));
518
+ }
519
+
520
+ function collectCallbackSymbols(collection: SymbolCollection): void {
521
+ const visit = (node: ts.Node): void => {
522
+ if (isTopLevelCallback(node)
523
+ && containsSupportedOutboundCall(node, collection.classifiedCalls)) {
524
+ const startLine = lineOf(
525
+ collection.source, node.getStart(collection.source),
526
+ );
527
+ const name = `callback:${startLine}`;
528
+ collection.symbols.push({
529
+ kind: 'callback', localName: name,
530
+ qualifiedName: `module:${collection.sourceFile}#${name}`,
531
+ sourceFile: collection.sourceFile, startLine,
532
+ endLine: lineOf(collection.source, node.getEnd()),
533
+ startOffset: node.getStart(collection.source), endOffset: node.getEnd(),
534
+ exported: false,
535
+ importExportEvidence: {
536
+ source: 'synthetic_outbound_callback', callbackLine: startLine,
537
+ },
538
+ });
425
539
  }
426
- ts.forEachChild(node, visitProxyVariables);
427
- };
428
- visitProxyVariables(source);
429
- const rememberClassInstance = (variableName: string, className: string, propertyName?: string): void => {
430
- const importSource = imports.get(className);
431
- if (!builtInConstructors.has(className) && ((importSource && isRelativeImport(importSource)) || declaredClasses.has(className))) classInstances.set(variableName, { className, importSource, propertyName });
540
+ ts.forEachChild(node, visit);
432
541
  };
433
- const visitClassInstances = (node: ts.Node): void => {
434
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isNewExpression(node.initializer) && ts.isIdentifier(node.initializer.expression)) {
435
- rememberClassInstance(node.name.text, node.initializer.expression.text);
436
- }
437
- if (ts.isPropertyDeclaration(node) && node.initializer && ts.isNewExpression(node.initializer) && ts.isIdentifier(node.initializer.expression)) {
438
- const propertyName = nameOf(node.name);
439
- if (propertyName) rememberClassInstance(`this.${propertyName}`, node.initializer.expression.text, propertyName);
440
- }
441
- ts.forEachChild(node, visitClassInstances);
542
+ visit(collection.source);
543
+ }
544
+
545
+ function createCollection(
546
+ source: ts.SourceFile,
547
+ sourceFile: string,
548
+ classifiedCalls: readonly ClassifiedOutboundCall[],
549
+ ): SymbolCollection {
550
+ return {
551
+ source, sourceFile, classifiedCalls,
552
+ symbols: [], imports: new Map(),
553
+ importBindings: collectSymbolImportBindings(source),
554
+ exportNames: exportDeclarations(source),
555
+ objectExports: new Set(), exportedClasses: new Set(),
556
+ declaredClasses: new Set(), proxies: new Map(), instances: new Map(),
442
557
  };
443
- visitClassInstances(source);
444
- const localCallables = new Set(symbols.flatMap((sym) => [sym.localName, sym.qualifiedName]));
445
- const visitCalls = (node: ts.Node): void => {
446
- if (ts.isCallExpression(node)) {
447
- const line = lineOf(source, node.getStart(source));
448
- const caller = nearest(symbols, line);
449
- if (caller) {
450
- const callee = callName(node.expression);
451
- const proxy = callee.local ? proxyVariables.get(callee.local) : undefined;
452
- const instance = (callee.local ? classInstances.get(callee.local) : undefined) ?? (callee.receiver ? classInstances.get(callee.receiver) : undefined);
453
- const importSource = instance?.importSource ?? proxy?.importSource ?? (callee.local ? imports.get(callee.local) : undefined) ?? (callee.member && callee.local ? imports.get(callee.local) : undefined);
454
- const directThisMethod = callee.receiver === 'this';
455
- const namespaceMember = Boolean(callee.member && callee.local && namespaceImports.has(callee.local));
456
- const packageImport = Boolean(importSource && !isRelativeImport(importSource));
457
- const targetName = instance && callee.member ? `${instance.className}.${callee.member}` : proxy && callee.member ? callee.member : directThisMethod ? callee.member : namespaceMember ? callee.member : packageImport ? (callee.member ?? callee.local) : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
458
- const className = caller.qualifiedName.includes('.') ? caller.qualifiedName.split('.')[0] : undefined;
459
- const thisTarget = directThisMethod && className && callee.member ? `${className}.${callee.member}` : undefined;
460
- const loggerLike = callee.receiver?.endsWith('.logger') || callee.local === 'logger' || (callee.expression.startsWith('this.logger.') && callee.member ? loggerMembers.has(callee.member) : false);
461
- const terminalMember = callee.member ? commonTerminalMembers.has(callee.member) || loggerMembers.has(callee.member) : false;
462
- const provenLocal = Boolean(targetName) && localCallables.has(String(targetName));
463
- const provenThisMethod = Boolean(thisTarget && localCallables.has(thisTarget));
464
- const provenRelativeImport = Boolean(isRelativeImport(importSource) && targetName);
465
- const provenClassInstance = Boolean(instance && callee.member && targetName);
466
- const provenPackageImport = Boolean(packageImport && targetName);
467
- const ignored = loggerLike || terminalMember || ignoredFrameworkCall(callee);
468
- const resolvedTarget = provenThisMethod ? thisTarget : targetName;
469
- const keep = Boolean(resolvedTarget) && !ignored && (provenLocal || provenThisMethod || provenRelativeImport || provenClassInstance || provenPackageImport);
470
- if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: resolvedTarget, receiverLocalName: callee.member ? (callee.local ?? callee.receiver) : undefined, importSource, sourceFile, sourceLine: line, callSiteStartOffset: node.getStart(source), callSiteEndOffset: node.getEnd(), callRole: 'ordinary_call', evidence: { relation: instance ? 'class_instance_method' : proxy ? 'relative_import_proxy_member' : packageImport ? 'package_import' : namespaceMember ? 'relative_import_namespace_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 } });
471
- }
472
- }
473
- ts.forEachChild(node, visitCalls);
558
+ }
559
+
560
+ function populateCollection(collection: SymbolCollection): void {
561
+ collectImportSources(collection);
562
+ collectDeclaredSymbols(collection);
563
+ collectCallbackSymbols(collection);
564
+ collectDerivedSymbolContexts(collection);
565
+ }
566
+
567
+ async function sourceFile(
568
+ repoPath: string,
569
+ filePath: string,
570
+ context?: RepositorySourceContext,
571
+ ): Promise<ts.SourceFile> {
572
+ const snapshot = context?.get(filePath);
573
+ const text = snapshot?.text
574
+ ?? await fs.readFile(path.join(repoPath, filePath), 'utf8');
575
+ return snapshot?.sourceFile() ?? ts.createSourceFile(
576
+ filePath, text, ts.ScriptTarget.Latest, true,
577
+ filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS,
578
+ );
579
+ }
580
+
581
+ export async function parseExecutableSymbols(
582
+ repoPath: string,
583
+ filePath: string,
584
+ context?: RepositorySourceContext,
585
+ preparedOutboundCalls?: readonly ClassifiedOutboundCall[],
586
+ ): Promise<{ symbols: ExecutableSymbolFact[]; calls: SymbolCallFact[] }> {
587
+ const source = await sourceFile(repoPath, filePath, context);
588
+ const normalizedFile = normalizePath(filePath);
589
+ const classified = preparedOutboundCalls
590
+ ?? classifyOutboundCallsInSource(source, normalizedFile);
591
+ const collection = createCollection(source, normalizedFile, classified);
592
+ populateCollection(collection);
593
+ const calls = collectSymbolCallFacts({
594
+ source, sourceFile: normalizedFile,
595
+ symbols: collection.symbols, imports: collection.imports,
596
+ importBindings: collection.importBindings,
597
+ proxies: collection.proxies, instances: collection.instances,
598
+ });
599
+ const events = reconcileEventSubscriptions(
600
+ source, classified, collection.symbols, calls,
601
+ );
602
+ return {
603
+ symbols: events.symbols,
604
+ calls: reconcileSymbolCallOwners(events.calls, events.symbols),
474
605
  };
475
- visitCalls(source);
476
- return { symbols, calls };
477
606
  }