@saptools/service-flow 0.1.67 → 0.1.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +27 -9
  3. package/TECHNICAL-NOTE.md +36 -0
  4. package/dist/chunk-3N3B5KHV.js +19596 -0
  5. package/dist/chunk-3N3B5KHV.js.map +1 -0
  6. package/dist/cli.js +2645 -521
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +67 -2
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/001-index-summary.ts +22 -0
  12. package/src/cli/003-doctor-package-resolution.ts +68 -0
  13. package/src/cli/doctor.ts +25 -14
  14. package/src/cli.ts +151 -87
  15. package/src/db/000-call-fact-repository.ts +499 -340
  16. package/src/db/001-fact-lifecycle.ts +60 -30
  17. package/src/db/002-fact-json-inventory.ts +169 -0
  18. package/src/db/003-current-fact-semantics.ts +699 -0
  19. package/src/db/004-package-target-invalidation.ts +183 -0
  20. package/src/db/005-schema-structure.ts +201 -0
  21. package/src/db/006-relative-symbol-resolution.ts +464 -0
  22. package/src/db/007-package-fact-semantics.ts +573 -0
  23. package/src/db/008-relative-fact-semantics.ts +210 -0
  24. package/src/db/009-binding-fact-semantics.ts +352 -0
  25. package/src/db/010-package-symbol-surface-semantics.ts +320 -0
  26. package/src/db/011-symbol-call-semantics.ts +144 -0
  27. package/src/db/012-binding-reference-proof.ts +268 -0
  28. package/src/db/013-index-publication-failure.ts +91 -0
  29. package/src/db/014-binding-helper-provenance.ts +17 -0
  30. package/src/db/migrations.ts +16 -3
  31. package/src/db/repositories.ts +130 -6
  32. package/src/db/schema.ts +4 -2
  33. package/src/index.ts +12 -0
  34. package/src/indexer/cds-extension-resolver.ts +27 -3
  35. package/src/indexer/repository-indexer.ts +135 -13
  36. package/src/indexer/workspace-indexer.ts +237 -34
  37. package/src/linker/003-package-import-symbol-resolver.ts +363 -131
  38. package/src/linker/004-event-subscription-handler-linker.ts +34 -11
  39. package/src/linker/005-odata-path-structure.ts +371 -0
  40. package/src/linker/006-event-template-link.ts +72 -0
  41. package/src/linker/007-call-edge-insertion.ts +568 -0
  42. package/src/linker/cross-repo-linker.ts +4 -166
  43. package/src/linker/odata-path-normalizer.ts +273 -180
  44. package/src/linker/service-resolver.ts +197 -77
  45. package/src/parsers/000-direct-query-execution.ts +11 -0
  46. package/src/parsers/002-symbol-import-bindings.ts +516 -0
  47. package/src/parsers/003-package-public-surface.ts +661 -0
  48. package/src/parsers/004-fact-identity.ts +108 -0
  49. package/src/parsers/005-event-subscription-facts.ts +281 -0
  50. package/src/parsers/006-binding-identity.ts +348 -0
  51. package/src/parsers/007-source-fact-reconciliation.ts +105 -0
  52. package/src/parsers/008-package-surface-publication.ts +82 -0
  53. package/src/parsers/009-symbol-call-facts.ts +528 -0
  54. package/src/parsers/010-package-public-surface-analysis.ts +352 -0
  55. package/src/parsers/011-binding-lexical-scope.ts +583 -0
  56. package/src/parsers/012-package-fact-contract.ts +306 -0
  57. package/src/parsers/013-executable-body-eligibility.ts +35 -0
  58. package/src/parsers/014-service-binding-helper-flow.ts +306 -0
  59. package/src/parsers/015-service-binding-collector.ts +693 -0
  60. package/src/parsers/016-local-symbol-reference.ts +261 -0
  61. package/src/parsers/017-symbol-derived-contexts.ts +268 -0
  62. package/src/parsers/018-package-commonjs-syntax.ts +142 -0
  63. package/src/parsers/019-binding-assignment-targets.ts +76 -0
  64. package/src/parsers/020-stable-local-value.ts +217 -0
  65. package/src/parsers/021-binding-visibility.ts +168 -0
  66. package/src/parsers/022-outbound-expression-analysis.ts +700 -0
  67. package/src/parsers/023-outbound-call-classifier.ts +692 -0
  68. package/src/parsers/operation-path-analysis.ts +6 -1
  69. package/src/parsers/outbound-call-parser.ts +162 -512
  70. package/src/parsers/package-json-parser.ts +45 -3
  71. package/src/parsers/service-binding-parser-helpers.ts +86 -15
  72. package/src/parsers/service-binding-parser.ts +147 -597
  73. package/src/parsers/symbol-parser.ts +513 -352
  74. package/src/trace/002-trace-diagnostics.ts +36 -1
  75. package/src/trace/007-implementation-start-diagnostic.ts +1 -0
  76. package/src/trace/011-event-subscriber-traversal.ts +100 -8
  77. package/src/trace/013-trace-root-scopes.ts +2 -3
  78. package/src/trace/014-compact-contract.ts +6 -0
  79. package/src/trace/015-trace-edge-recorder.ts +61 -4
  80. package/src/trace/016-compact-projector.ts +15 -17
  81. package/src/trace/019-trace-edge-semantics.ts +6 -10
  82. package/src/trace/020-compact-field-projection.ts +122 -38
  83. package/src/trace/021-compact-decision-normalization.ts +171 -0
  84. package/src/trace/022-trace-fact-preflight.ts +21 -0
  85. package/src/trace/023-nested-event-scopes.ts +23 -0
  86. package/src/trace/024-compact-observation-decision.ts +81 -0
  87. package/src/trace/025-trace-implementation-scope.ts +123 -0
  88. package/src/trace/026-trace-start-scope.ts +336 -0
  89. package/src/trace/027-trace-scope-execution.ts +566 -0
  90. package/src/trace/028-trace-operation-execution.ts +336 -0
  91. package/src/trace/029-trace-start-implementation.ts +172 -0
  92. package/src/trace/030-event-runtime-resolution.ts +151 -0
  93. package/src/trace/031-local-call-expansion.ts +37 -0
  94. package/src/trace/implementation-hints.ts +9 -6
  95. package/src/trace/selectors.ts +1 -0
  96. package/src/trace/trace-engine.ts +122 -624
  97. package/src/types.ts +57 -0
  98. package/src/utils/001-placeholders.ts +188 -10
  99. package/src/version.ts +1 -1
  100. package/dist/chunk-ZQABU7MR.js +0 -12151
  101. 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;
@@ -23,6 +42,12 @@ function isFunctionLike(node: ts.Node): node is ts.FunctionLikeDeclaration {
23
42
  function exported(node: ts.Node): boolean {
24
43
  return Boolean(ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export);
25
44
  }
45
+ function defaultExported(node: ts.Node): boolean {
46
+ return Boolean(
47
+ ts.getCombinedModifierFlags(node as ts.Declaration)
48
+ & ts.ModifierFlags.Default,
49
+ );
50
+ }
26
51
  function isPublicClassMethod(node: ts.MethodDeclaration): boolean {
27
52
  const flags = ts.getCombinedModifierFlags(node);
28
53
  return (flags & ts.ModifierFlags.Private) === 0 && (flags & ts.ModifierFlags.Protected) === 0;
@@ -38,80 +63,9 @@ function exportDeclarations(source: ts.SourceFile): Map<string, string> {
38
63
  visit(source);
39
64
  return exports;
40
65
  }
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 {
66
+ function isObjectFunction(
67
+ node: ts.Node,
68
+ ): node is ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration {
115
69
  return ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node);
116
70
  }
117
71
  type ParameterBinding =
@@ -119,69 +73,11 @@ type ParameterBinding =
119
73
  | { index: number; kind: 'object_pattern'; properties: Array<{ property: string; local: string }> }
120
74
  | { index: number; kind: 'array_pattern'; elements: Array<{ index: number; local: string }> };
121
75
  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
76
  function requireSource(expr: ts.Expression): string | undefined {
147
77
  if (!ts.isCallExpression(expr) || !ts.isIdentifier(expr.expression) || expr.expression.text !== 'require') return undefined;
148
78
  const first = expr.arguments[0];
149
79
  return first && ts.isStringLiteral(first) ? first.text : undefined;
150
80
  }
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
81
  function bindingLocalName(name: ts.BindingName, initializer?: ts.Expression): string | undefined {
186
82
  if (ts.isIdentifier(name)) return name.text;
187
83
  if (initializer && ts.isIdentifier(initializer)) return initializer.text;
@@ -241,237 +137,502 @@ function parameterBindings(params: ts.NodeArray<ts.ParameterDeclaration>): Param
241
137
  return [];
242
138
  });
243
139
  }
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 });
140
+ interface SymbolCollection {
141
+ source: ts.SourceFile;
142
+ sourceFile: string;
143
+ symbols: ExecutableSymbolFact[];
144
+ imports: Map<string, string>;
145
+ importBindings: SymbolImportBinding[];
146
+ classifiedCalls: readonly ClassifiedOutboundCall[];
147
+ exportNames: Map<string, string>;
148
+ objectExports: Set<string>;
149
+ exportedClasses: Set<string>;
150
+ defaultExportedClasses: Set<string>;
151
+ declaredClasses: Set<string>;
152
+ proxies: Map<string, SymbolCallProxy[]>;
153
+ instances: Map<string, SymbolClassInstance[]>;
154
+ }
155
+
156
+ function symbolSourceEvidence(
157
+ collection: SymbolCollection,
158
+ node: ts.Node,
159
+ options: {
160
+ parentRoot: string;
161
+ qualifiedName: string;
162
+ declaredExportName?: string;
163
+ classContainerExported: boolean; classContainerDefaultExported: boolean;
164
+ classMemberExported: boolean;
165
+ objectExported: boolean;
166
+ evidence?: Record<string, unknown>;
167
+ },
168
+ ): Record<string, unknown> {
169
+ if (options.evidence) return options.evidence;
170
+ if (options.classMemberExported) return {
171
+ source: 'exported_class_member',
172
+ exportedClass: options.parentRoot,
173
+ memberKind: ts.getCombinedModifierFlags(node as ts.Declaration)
174
+ & ts.ModifierFlags.Static ? 'static_method' : 'class_method',
285
175
  };
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 } });
176
+ if (options.classContainerExported && ts.isMethodDeclaration(node)
177
+ && isPublicClassMethod(node)) return {
178
+ source: 'exported_class_instance_member', exportedClass: options.parentRoot,
179
+ exportedClassExportKind: options.classContainerDefaultExported
180
+ ? 'default' : 'named',
181
+ memberKind: 'class_method',
288
182
  };
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);
183
+ if (options.declaredExportName) return {
184
+ exportedName: options.declaredExportName,
185
+ source: 'export_declaration',
309
186
  };
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));
187
+ return options.objectExported
188
+ ? { exportedName: options.qualifiedName, source: 'exported_object_literal' }
189
+ : {};
190
+ }
191
+
192
+ function executableEvidence(
193
+ node: ts.Node,
194
+ source: ts.SourceFile,
195
+ ): Record<string, unknown> {
196
+ if (!isFunctionLike(node)) return {};
197
+ const bindings = parameterBindings(node.parameters);
198
+ const parameters = bindings.flatMap((binding) =>
199
+ binding.kind === 'identifier' ? [binding.name] : []);
200
+ const aliases = parameterPropertyAliases(node, source);
201
+ return {
202
+ executableBodyEligibility: executableBodyEligibility(node, source),
203
+ ...(bindings.length > 0 ? { parameters, parameterBindings: bindings } : {}),
204
+ ...(aliases.length > 0 ? { parameterPropertyAliases: aliases } : {}),
356
205
  };
357
- visitSymbols(source);
206
+ }
207
+
208
+ interface SymbolNames {
209
+ parentRoot: string;
210
+ qualifiedName: string;
211
+ declaredExportName?: string;
212
+ objectExported: boolean;
213
+ classContainerExported: boolean;
214
+ classContainerDefaultExported: boolean;
215
+ classMemberExported: boolean;
216
+ effectiveName?: string;
217
+ }
218
+
219
+ function exportedClassMember(
220
+ collection: SymbolCollection,
221
+ kind: string,
222
+ parentName: string | undefined,
223
+ parentRoot: string,
224
+ node: ts.Node,
225
+ ): boolean {
226
+ if (kind !== 'method' || !parentName
227
+ || !collection.exportedClasses.has(parentRoot)
228
+ || !ts.isMethodDeclaration(node)) return false;
229
+ return Boolean(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Static)
230
+ && isPublicClassMethod(node);
231
+ }
358
232
 
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));
233
+ function classExportState(
234
+ collection: SymbolCollection,
235
+ parentName: string | undefined,
236
+ parentRoot: string,
237
+ ): {
238
+ classContainerExported: boolean;
239
+ classContainerDefaultExported: boolean;
240
+ } {
241
+ return {
242
+ classContainerExported: Boolean(
243
+ parentName && collection.exportedClasses.has(parentRoot),
244
+ ),
245
+ classContainerDefaultExported: Boolean(
246
+ parentName && collection.defaultExportedClasses.has(parentRoot),
247
+ ),
365
248
  };
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);
249
+ }
250
+
251
+ function symbolNames(
252
+ collection: SymbolCollection,
253
+ kind: string,
254
+ localName: string,
255
+ node: ts.Node,
256
+ parentName?: string,
257
+ exportedName?: string,
258
+ ): SymbolNames {
259
+ const parentRoot = parentName?.split('.')[0] ?? '';
260
+ const declaredExportName = exportedName ?? collection.exportNames.get(
261
+ parentName ? parentRoot : localName,
262
+ );
263
+ const qualifiedName = parentName ? `${parentName}.${localName}` : localName;
264
+ const objectExported = Boolean(
265
+ parentName && collection.objectExports.has(parentRoot),
266
+ );
267
+ const classMemberExported = exportedClassMember(
268
+ collection, kind, parentName, parentRoot, node,
269
+ );
270
+ const classState = classExportState(collection, parentName, parentRoot);
271
+ return {
272
+ parentRoot, declaredExportName, qualifiedName,
273
+ objectExported, ...classState,
274
+ classMemberExported,
275
+ effectiveName: classMemberExported || objectExported
276
+ ? qualifiedName : declaredExportName,
373
277
  };
374
- visitCallbackSymbols(source);
278
+ }
375
279
 
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);
280
+ function addExecutableSymbol(
281
+ collection: SymbolCollection,
282
+ kind: string,
283
+ localName: string,
284
+ node: ts.Node,
285
+ parentName?: string,
286
+ exportedName?: string,
287
+ evidence?: Record<string, unknown>,
288
+ ): void {
289
+ const names = symbolNames(
290
+ collection, kind, localName, node, parentName, exportedName,
291
+ );
292
+ const sourceEvidence = symbolSourceEvidence(collection, node, {
293
+ parentRoot: names.parentRoot,
294
+ qualifiedName: names.qualifiedName,
295
+ declaredExportName: names.declaredExportName,
296
+ classContainerExported: names.classContainerExported,
297
+ classContainerDefaultExported: names.classContainerDefaultExported,
298
+ classMemberExported: names.classMemberExported,
299
+ objectExported: names.objectExported,
300
+ evidence,
301
+ });
302
+ collection.symbols.push({
303
+ kind,
304
+ localName: kind === 'object_method' ? names.qualifiedName : localName,
305
+ exportedName: names.effectiveName,
306
+ qualifiedName: names.qualifiedName,
307
+ sourceFile: collection.sourceFile,
308
+ startLine: lineOf(collection.source, node.getStart(collection.source)),
309
+ endLine: lineOf(collection.source, node.getEnd()),
310
+ startOffset: node.getStart(collection.source),
311
+ endOffset: node.getEnd(),
312
+ exported: exported(node) || Boolean(names.effectiveName),
313
+ importExportEvidence: {
314
+ ...sourceEvidence,
315
+ ...executableEvidence(node, collection.source),
316
+ },
317
+ });
318
+ }
319
+
320
+ function addAliasSymbol(
321
+ collection: SymbolCollection,
322
+ objectName: string,
323
+ propertyName: string,
324
+ node: ts.Node,
325
+ ): void {
326
+ collection.symbols.push({
327
+ kind: 'object_alias',
328
+ localName: propertyName,
329
+ exportedName: propertyName,
330
+ qualifiedName: `${objectName}.${propertyName}`,
331
+ sourceFile: collection.sourceFile,
332
+ startLine: lineOf(collection.source, node.getStart(collection.source)),
333
+ endLine: lineOf(collection.source, node.getEnd()),
334
+ startOffset: node.getStart(collection.source),
335
+ endOffset: node.getEnd(),
336
+ exported: true,
337
+ importExportEvidence: {
338
+ source: 'exported_object_shorthand',
339
+ objectName,
340
+ propertyName,
341
+ targetImportSource: collection.imports.get(propertyName),
342
+ },
343
+ });
344
+ }
345
+
346
+ function collectImportSources(collection: SymbolCollection): void {
347
+ const visit = (node: ts.Node): void => {
348
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier))
349
+ collectEsmImportSources(collection.imports, node);
350
+ if (ts.isVariableStatement(node))
351
+ collectCjsImportSources(collection.imports, node);
352
+ ts.forEachChild(node, visit);
418
353
  };
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 });
354
+ visit(collection.source);
355
+ }
356
+
357
+ function collectEsmImportSources(
358
+ imports: Map<string, string>,
359
+ node: ts.ImportDeclaration,
360
+ ): void {
361
+ if (!ts.isStringLiteral(node.moduleSpecifier)) return;
362
+ const source = node.moduleSpecifier.text;
363
+ const clause = node.importClause;
364
+ if (clause?.name) imports.set(clause.name.text, source);
365
+ const named = clause?.namedBindings;
366
+ if (named && ts.isNamedImports(named))
367
+ for (const item of named.elements) imports.set(item.name.text, source);
368
+ if (named && ts.isNamespaceImport(named))
369
+ imports.set(named.name.text, source);
370
+ }
371
+
372
+ function collectCjsImportSources(
373
+ imports: Map<string, string>,
374
+ node: ts.VariableStatement,
375
+ ): void {
376
+ for (const declaration of node.declarationList.declarations) {
377
+ const source = declaration.initializer
378
+ ? requireSource(declaration.initializer) : undefined;
379
+ if (!source) continue;
380
+ if (ts.isIdentifier(declaration.name))
381
+ imports.set(declaration.name.text, source);
382
+ if (ts.isObjectBindingPattern(declaration.name))
383
+ for (const item of declaration.name.elements)
384
+ if (ts.isIdentifier(item.name)) imports.set(item.name.text, source);
385
+ }
386
+ }
387
+
388
+ function classPropertySymbol(
389
+ collection: SymbolCollection,
390
+ node: ts.PropertyDeclaration,
391
+ parentClass: string,
392
+ ): void {
393
+ const initializer = node.initializer;
394
+ const localName = nameOf(node.name);
395
+ if (!localName || !initializer
396
+ || (!ts.isArrowFunction(initializer)
397
+ && !ts.isFunctionExpression(initializer))) return;
398
+ const staticPublic = publicStaticProperty(collection, node, parentClass);
399
+ const memberKind = propertyMemberKind(initializer, staticPublic);
400
+ addExecutableSymbol(
401
+ collection, 'method', localName, initializer, parentClass,
402
+ staticPublic ? `${parentClass}.${localName}` : undefined,
403
+ staticPublic
404
+ ? { source: 'exported_class_member', exportedClass: parentClass, memberKind }
405
+ : { source: 'class_property_function', memberKind },
406
+ );
407
+ }
408
+
409
+ function publicStaticProperty(
410
+ collection: SymbolCollection,
411
+ node: ts.PropertyDeclaration,
412
+ parentClass: string,
413
+ ): boolean {
414
+ const flags = ts.getCombinedModifierFlags(node);
415
+ return collection.exportedClasses.has(parentClass)
416
+ && Boolean(flags & ts.ModifierFlags.Static)
417
+ && (flags & ts.ModifierFlags.Private) === 0
418
+ && (flags & ts.ModifierFlags.Protected) === 0;
419
+ }
420
+
421
+ function propertyMemberKind(
422
+ initializer: ts.ArrowFunction | ts.FunctionExpression,
423
+ staticPublic: boolean,
424
+ ): string {
425
+ if (ts.isArrowFunction(initializer))
426
+ return staticPublic ? 'static_arrow_function' : 'arrow_function_property';
427
+ return staticPublic
428
+ ? 'static_function_expression'
429
+ : 'function_expression_property';
430
+ }
431
+
432
+ function objectCallable(
433
+ property: ts.ObjectLiteralElementLike,
434
+ ): ts.FunctionLikeDeclaration | undefined {
435
+ if (ts.isMethodDeclaration(property)) return property;
436
+ return ts.isPropertyAssignment(property)
437
+ && isObjectFunction(property.initializer)
438
+ ? property.initializer
439
+ : undefined;
440
+ }
441
+
442
+ function objectLiteralSymbols(
443
+ collection: SymbolCollection,
444
+ objectName: string,
445
+ object: ts.ObjectLiteralExpression,
446
+ objectIsExported: boolean,
447
+ ): void {
448
+ if (objectIsExported) collection.objectExports.add(objectName);
449
+ for (const property of object.properties) {
450
+ if (objectIsExported && ts.isShorthandPropertyAssignment(property))
451
+ addAliasSymbol(collection, objectName, property.name.text, property.name);
452
+ const callable = objectCallable(property);
453
+ const propertyName = callable ? nameOf(property.name) : undefined;
454
+ if (callable && propertyName)
455
+ addExecutableSymbol(
456
+ collection, 'object_method', propertyName, callable, objectName,
457
+ );
458
+ }
459
+ }
460
+
461
+ function variableSymbols(
462
+ collection: SymbolCollection,
463
+ node: ts.VariableStatement,
464
+ ): void {
465
+ for (const declaration of node.declarationList.declarations) {
466
+ const localName = nameOf(declaration.name);
467
+ const initializer = declaration.initializer;
468
+ if (!localName || !initializer) continue;
469
+ if (isFunctionLike(initializer)) addExecutableSymbol(
470
+ collection, 'function', localName, initializer, undefined,
471
+ exported(node) ? localName : collection.exportNames.get(localName),
472
+ );
473
+ if (ts.isObjectLiteralExpression(initializer))
474
+ objectLiteralSymbols(
475
+ collection, localName, initializer,
476
+ exported(node) || collection.exportNames.has(localName),
477
+ );
478
+ }
479
+ }
480
+
481
+ function collectClassDeclaration(
482
+ collection: SymbolCollection,
483
+ node: ts.Node,
484
+ ): boolean {
485
+ if (!ts.isClassDeclaration(node) || !node.name) return false;
486
+ collection.declaredClasses.add(node.name.text);
487
+ if (exported(node) || collection.exportNames.has(node.name.text))
488
+ collection.exportedClasses.add(node.name.text);
489
+ if (defaultExported(node))
490
+ collection.defaultExportedClasses.add(node.name.text);
491
+ for (const member of node.members)
492
+ visitDeclaredSymbol(collection, member, node.name.text);
493
+ return true;
494
+ }
495
+
496
+ function collectMethodDeclaration(
497
+ collection: SymbolCollection,
498
+ node: ts.Node,
499
+ parentClass?: string,
500
+ ): boolean {
501
+ if (!ts.isMethodDeclaration(node)) return false;
502
+ const localName = nameOf(node.name);
503
+ if (localName)
504
+ addExecutableSymbol(collection, 'method', localName, node, parentClass);
505
+ return true;
506
+ }
507
+
508
+ function visitDeclaredSymbol(
509
+ collection: SymbolCollection,
510
+ node: ts.Node,
511
+ parentClass?: string,
512
+ ): void {
513
+ if (collectClassDeclaration(collection, node)) return;
514
+ if (collectMethodDeclaration(collection, node, parentClass)) return;
515
+ if (ts.isPropertyDeclaration(node)) {
516
+ if (parentClass) classPropertySymbol(collection, node, parentClass);
517
+ return;
518
+ }
519
+ if (ts.isFunctionDeclaration(node) && node.name) {
520
+ addExecutableSymbol(
521
+ collection, 'function', node.name.text, node, undefined,
522
+ exported(node) ? node.name.text : undefined,
523
+ );
524
+ return;
525
+ }
526
+ if (ts.isVariableStatement(node)) {
527
+ variableSymbols(collection, node);
528
+ return;
529
+ }
530
+ ts.forEachChild(node, (child) =>
531
+ visitDeclaredSymbol(collection, child, parentClass));
532
+ }
533
+
534
+ function collectDeclaredSymbols(collection: SymbolCollection): void {
535
+ visitDeclaredSymbol(collection, collection.source);
536
+ }
537
+
538
+ function isTopLevelCallback(
539
+ node: ts.Node,
540
+ ): node is ts.ArrowFunction | ts.FunctionExpression {
541
+ if ((!ts.isArrowFunction(node) && !ts.isFunctionExpression(node))
542
+ || !ts.isCallExpression(node.parent)) return false;
543
+ const callee = symbolCallName(node.parent.expression);
544
+ const member = callee.member ?? callee.local;
545
+ return Boolean(member && [
546
+ 'bootstrap', 'served', 'connect', 'on', 'once', 'use',
547
+ 'get', 'post', 'put', 'patch', 'delete', 'subscribe',
548
+ ].includes(member));
549
+ }
550
+
551
+ function collectCallbackSymbols(collection: SymbolCollection): void {
552
+ const visit = (node: ts.Node): void => {
553
+ if (isTopLevelCallback(node)
554
+ && containsSupportedOutboundCall(node, collection.classifiedCalls)) {
555
+ const startLine = lineOf(
556
+ collection.source, node.getStart(collection.source),
557
+ );
558
+ const name = `callback:${startLine}`;
559
+ collection.symbols.push({
560
+ kind: 'callback', localName: name,
561
+ qualifiedName: `module:${collection.sourceFile}#${name}`,
562
+ sourceFile: collection.sourceFile, startLine,
563
+ endLine: lineOf(collection.source, node.getEnd()),
564
+ startOffset: node.getStart(collection.source), endOffset: node.getEnd(),
565
+ exported: false,
566
+ importExportEvidence: {
567
+ source: 'synthetic_outbound_callback', callbackLine: startLine,
568
+ },
569
+ });
425
570
  }
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 });
571
+ ts.forEachChild(node, visit);
432
572
  };
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);
573
+ visit(collection.source);
574
+ }
575
+
576
+ function createCollection(
577
+ source: ts.SourceFile,
578
+ sourceFile: string,
579
+ classifiedCalls: readonly ClassifiedOutboundCall[],
580
+ ): SymbolCollection {
581
+ return {
582
+ source, sourceFile, classifiedCalls,
583
+ symbols: [], imports: new Map(),
584
+ importBindings: collectSymbolImportBindings(source),
585
+ exportNames: exportDeclarations(source),
586
+ objectExports: new Set(), exportedClasses: new Set(),
587
+ defaultExportedClasses: new Set(),
588
+ declaredClasses: new Set(), proxies: new Map(), instances: new Map(),
442
589
  };
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);
590
+ }
591
+
592
+ function populateCollection(collection: SymbolCollection): void {
593
+ collectImportSources(collection);
594
+ collectDeclaredSymbols(collection);
595
+ collectCallbackSymbols(collection);
596
+ collectDerivedSymbolContexts(collection);
597
+ }
598
+
599
+ async function sourceFile(
600
+ repoPath: string,
601
+ filePath: string,
602
+ context?: RepositorySourceContext,
603
+ ): Promise<ts.SourceFile> {
604
+ const snapshot = context?.get(filePath);
605
+ const text = snapshot?.text
606
+ ?? await fs.readFile(path.join(repoPath, filePath), 'utf8');
607
+ return snapshot?.sourceFile() ?? ts.createSourceFile(
608
+ filePath, text, ts.ScriptTarget.Latest, true,
609
+ filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS,
610
+ );
611
+ }
612
+
613
+ export async function parseExecutableSymbols(
614
+ repoPath: string,
615
+ filePath: string,
616
+ context?: RepositorySourceContext,
617
+ preparedOutboundCalls?: readonly ClassifiedOutboundCall[],
618
+ ): Promise<{ symbols: ExecutableSymbolFact[]; calls: SymbolCallFact[] }> {
619
+ const source = await sourceFile(repoPath, filePath, context);
620
+ const normalizedFile = normalizePath(filePath);
621
+ const classified = preparedOutboundCalls
622
+ ?? classifyOutboundCallsInSource(source, normalizedFile);
623
+ const collection = createCollection(source, normalizedFile, classified);
624
+ populateCollection(collection);
625
+ const calls = collectSymbolCallFacts({
626
+ source, sourceFile: normalizedFile,
627
+ symbols: collection.symbols, imports: collection.imports,
628
+ importBindings: collection.importBindings,
629
+ proxies: collection.proxies, instances: collection.instances,
630
+ });
631
+ const events = reconcileEventSubscriptions(
632
+ source, classified, collection.symbols, calls,
633
+ );
634
+ return {
635
+ symbols: events.symbols,
636
+ calls: reconcileSymbolCallOwners(events.calls, events.symbols),
474
637
  };
475
- visitCalls(source);
476
- return { symbols, calls };
477
638
  }