@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
@@ -0,0 +1,583 @@
1
+ import ts from 'typescript';
2
+ import type { LexicalScopeFact } from '../types.js';
3
+ import {
4
+ bindingAssignmentEntries,
5
+ type BindingAssignmentEntry,
6
+ } from './019-binding-assignment-targets.js';
7
+
8
+ export type BindingDeclarationKind =
9
+ | 'const'
10
+ | 'let'
11
+ | 'var'
12
+ | 'parameter'
13
+ | 'catch'
14
+ | 'value_shadow';
15
+
16
+ export interface BindingLexicalSite {
17
+ variableName: string;
18
+ node: ts.Node;
19
+ startOffset: number;
20
+ endOffset: number;
21
+ flow: 'declaration' | 'assignment' | 'shadow';
22
+ declarationKind?: BindingDeclarationKind;
23
+ aliasSource?: string;
24
+ aliasKind?: 'identity'
25
+ | 'identity-assignment'
26
+ | 'transaction'
27
+ | 'array-destructuring';
28
+ aliasArrayIndex?: number;
29
+ aliasPromiseAll?: boolean;
30
+ deterministic: boolean;
31
+ scopeChain: LexicalScopeFact[];
32
+ executionScope: LexicalScopeFact;
33
+ caseClauseStartOffset?: number;
34
+ caseClauseEndOffset?: number;
35
+ declarationKey?: string;
36
+ }
37
+
38
+ export interface BindingSiteCandidate<T> {
39
+ variableName: string;
40
+ bindingSiteStartOffset?: number;
41
+ bindingSiteEndOffset?: number;
42
+ value: T;
43
+ }
44
+
45
+ export interface BindingLexicalIndex {
46
+ source: ts.SourceFile;
47
+ sites: BindingLexicalSite[];
48
+ }
49
+
50
+ export interface VisibleBinding<T> {
51
+ status: 'resolved' | 'ambiguous' | 'unresolved';
52
+ reason?: 'binding_not_found'
53
+ | 'binding_declared_after_call'
54
+ | 'unsupported_reaching_assignment'
55
+ | 'unsupported_var_binding'
56
+ | 'binding_flow_unsupported';
57
+ candidate?: BindingSiteCandidate<T>;
58
+ site?: BindingLexicalSite;
59
+ declarationSite?: BindingLexicalSite;
60
+ scopeIndex?: number;
61
+ }
62
+
63
+ function unwrapIdentity(expression: ts.Expression): ts.Expression {
64
+ if (ts.isParenthesizedExpression(expression)
65
+ || ts.isAwaitExpression(expression)
66
+ || ts.isAsExpression(expression)
67
+ || ts.isSatisfiesExpression(expression)
68
+ || ts.isTypeAssertionExpression(expression))
69
+ return unwrapIdentity(expression.expression);
70
+ return expression;
71
+ }
72
+
73
+ function transactionSource(expression: ts.Expression | undefined): string | undefined {
74
+ if (!expression) return undefined;
75
+ const unwrapped = unwrapIdentity(expression);
76
+ if (ts.isConditionalExpression(unwrapped)) {
77
+ const left = transactionSource(unwrapped.whenTrue);
78
+ const right = transactionSource(unwrapped.whenFalse);
79
+ return left && left === right ? left : undefined;
80
+ }
81
+ if (!ts.isCallExpression(unwrapped)
82
+ || !ts.isPropertyAccessExpression(unwrapped.expression)) return undefined;
83
+ const receiver = unwrapped.expression;
84
+ return ['tx', 'transaction'].includes(receiver.name.text)
85
+ && ts.isIdentifier(receiver.expression)
86
+ ? receiver.expression.text
87
+ : undefined;
88
+ }
89
+
90
+ function directAlias(expression: ts.Expression | undefined): string | undefined {
91
+ if (!expression) return undefined;
92
+ const unwrapped = unwrapIdentity(expression);
93
+ return ts.isIdentifier(unwrapped)
94
+ ? unwrapped.text
95
+ : transactionSource(unwrapped);
96
+ }
97
+
98
+ function scopeKind(node: ts.Node): LexicalScopeFact['kind'] | undefined {
99
+ if (ts.isSourceFile(node)) return 'source_file';
100
+ if (ts.isModuleBlock(node)) return 'module_block';
101
+ if (ts.isFunctionLike(node)) return 'function';
102
+ if (ts.isClassLike(node)) return 'class';
103
+ if (ts.isIterationStatement(node, false)) return 'loop';
104
+ if (ts.isCaseBlock(node)) return 'case_block';
105
+ if (ts.isBlock(node)) return 'block';
106
+ return ts.isCatchClause(node) ? 'catch' : undefined;
107
+ }
108
+
109
+ function lexicalScope(node: ts.Node, source: ts.SourceFile): LexicalScopeFact | undefined {
110
+ const kind = scopeKind(node);
111
+ if (!kind) return undefined;
112
+ return {
113
+ kind,
114
+ startOffset: ts.isSourceFile(node) ? 0 : node.getStart(source),
115
+ endOffset: node.getEnd(),
116
+ };
117
+ }
118
+
119
+ export function lexicalScopeChain(
120
+ node: ts.Node,
121
+ source: ts.SourceFile,
122
+ ): LexicalScopeFact[] {
123
+ const scopes: LexicalScopeFact[] = [];
124
+ let current: ts.Node | undefined = node;
125
+ while (current) {
126
+ const scope = lexicalScope(current, source);
127
+ if (scope) scopes.push(scope);
128
+ current = current.parent;
129
+ }
130
+ return scopes.reverse();
131
+ }
132
+
133
+ export function sameScope(
134
+ left: LexicalScopeFact,
135
+ right: LexicalScopeFact,
136
+ ): boolean {
137
+ return left.kind === right.kind
138
+ && left.startOffset === right.startOffset
139
+ && left.endOffset === right.endOffset;
140
+ }
141
+
142
+ export function executionScope(
143
+ chain: readonly LexicalScopeFact[],
144
+ ): LexicalScopeFact {
145
+ return [...chain].reverse().find((scope) =>
146
+ scope.kind === 'function' || scope.kind === 'source_file')
147
+ ?? { kind: 'source_file', startOffset: 0, endOffset: 0 };
148
+ }
149
+
150
+ function declarationKind(node: ts.VariableDeclaration): BindingDeclarationKind {
151
+ const flags = node.parent.flags;
152
+ if ((flags & ts.NodeFlags.Const) !== 0) return 'const';
153
+ return (flags & ts.NodeFlags.Let) !== 0 ? 'let' : 'var';
154
+ }
155
+
156
+ const shortCircuitOperators = new Set<ts.SyntaxKind>([
157
+ ts.SyntaxKind.AmpersandAmpersandToken,
158
+ ts.SyntaxKind.BarBarToken,
159
+ ts.SyntaxKind.QuestionQuestionToken,
160
+ ts.SyntaxKind.AmpersandAmpersandEqualsToken,
161
+ ts.SyntaxKind.BarBarEqualsToken,
162
+ ts.SyntaxKind.QuestionQuestionEqualsToken,
163
+ ]);
164
+
165
+ function shortCircuitRightBranch(
166
+ descendant: ts.Node,
167
+ node: ts.Node,
168
+ ): boolean {
169
+ return ts.isBinaryExpression(node)
170
+ && shortCircuitOperators.has(node.operatorToken.kind)
171
+ && descendant.pos >= node.right.pos
172
+ && descendant.end <= node.right.end;
173
+ }
174
+
175
+ function optionalChainAncestor(node: ts.Node): boolean {
176
+ return (ts.isCallExpression(node)
177
+ || ts.isPropertyAccessExpression(node)
178
+ || ts.isElementAccessExpression(node))
179
+ && Boolean(node.questionDotToken);
180
+ }
181
+
182
+ function deferredClassInitializer(node: ts.Node): boolean {
183
+ return ts.isPropertyDeclaration(node)
184
+ && (ts.getCombinedModifierFlags(node)
185
+ & ts.ModifierFlags.Static) === 0;
186
+ }
187
+
188
+ function controlFlowAncestor(node: ts.Node): boolean {
189
+ return ts.isIfStatement(node)
190
+ || ts.isConditionalExpression(node)
191
+ || ts.isSwitchStatement(node)
192
+ || ts.isIterationStatement(node, false)
193
+ || ts.isTryStatement(node)
194
+ || deferredClassInitializer(node);
195
+ }
196
+
197
+ function branchDependent(node: ts.Node): boolean {
198
+ let descendant = node;
199
+ let current: ts.Node | undefined = node.parent;
200
+ while (current && !ts.isSourceFile(current) && !ts.isFunctionLike(current)) {
201
+ if (controlFlowAncestor(current)
202
+ || shortCircuitRightBranch(descendant, current)
203
+ || optionalChainAncestor(current)) return true;
204
+ descendant = current;
205
+ current = current.parent;
206
+ }
207
+ return false;
208
+ }
209
+
210
+ function bindingEntries(name: ts.BindingName): Array<{
211
+ variableName: string;
212
+ element?: ts.BindingElement;
213
+ arrayIndex?: number;
214
+ }> {
215
+ if (ts.isIdentifier(name)) return [{ variableName: name.text }];
216
+ return name.elements.flatMap((element, arrayIndex) =>
217
+ ts.isBindingElement(element)
218
+ ? bindingEntries(element.name).map((entry) => ({
219
+ ...entry,
220
+ element,
221
+ ...(ts.isArrayBindingPattern(name) ? { arrayIndex } : {}),
222
+ }))
223
+ : []);
224
+ }
225
+
226
+ function arrayContainer(
227
+ expression: ts.Expression | undefined,
228
+ ): { array: ts.ArrayLiteralExpression; promiseAll: boolean } | undefined {
229
+ if (!expression) return undefined;
230
+ const unwrapped = unwrapIdentity(expression);
231
+ if (ts.isArrayLiteralExpression(unwrapped))
232
+ return { array: unwrapped, promiseAll: false };
233
+ if (!ts.isCallExpression(unwrapped)
234
+ || !ts.isPropertyAccessExpression(unwrapped.expression)
235
+ || unwrapped.expression.name.text !== 'all'
236
+ || unwrapped.expression.expression.getText() !== 'Promise') return undefined;
237
+ const first = unwrapped.arguments[0];
238
+ if (!first) return undefined;
239
+ const container = unwrapIdentity(first);
240
+ return ts.isArrayLiteralExpression(container)
241
+ ? { array: container, promiseAll: true }
242
+ : undefined;
243
+ }
244
+
245
+ function entryAlias(
246
+ node: ts.VariableDeclaration,
247
+ entry: {
248
+ variableName: string;
249
+ element?: ts.BindingElement;
250
+ arrayIndex?: number;
251
+ },
252
+ ): string | undefined {
253
+ if (!entry.element) return directAlias(node.initializer);
254
+ if (!ts.isArrayBindingPattern(node.name)) return undefined;
255
+ const expression = entry.arrayIndex === undefined
256
+ ? undefined
257
+ : arrayContainer(node.initializer)?.array.elements[entry.arrayIndex];
258
+ return expression && !ts.isOmittedExpression(expression)
259
+ ? directAlias(expression)
260
+ : undefined;
261
+ }
262
+
263
+ function siteBase(
264
+ source: ts.SourceFile,
265
+ node: ts.Node,
266
+ ): Pick<BindingLexicalSite, 'node' | 'startOffset' | 'endOffset'
267
+ | 'scopeChain' | 'executionScope'
268
+ | 'caseClauseStartOffset' | 'caseClauseEndOffset'> {
269
+ const scopeChain = lexicalScopeChain(node, source);
270
+ const clause = enclosingCaseClause(node);
271
+ return {
272
+ node,
273
+ startOffset: node.getStart(source),
274
+ endOffset: node.getEnd(),
275
+ scopeChain,
276
+ executionScope: executionScope(scopeChain),
277
+ caseClauseStartOffset: clause?.getStart(source),
278
+ caseClauseEndOffset: clause?.getEnd(),
279
+ };
280
+ }
281
+
282
+ export function enclosingCaseClause(
283
+ node: ts.Node,
284
+ ): ts.CaseClause | ts.DefaultClause | undefined {
285
+ let current: ts.Node | undefined = node.parent;
286
+ while (current && !ts.isSourceFile(current)
287
+ && !ts.isFunctionLike(current)) {
288
+ if (ts.isCaseClause(current) || ts.isDefaultClause(current))
289
+ return current;
290
+ current = current.parent;
291
+ }
292
+ return undefined;
293
+ }
294
+
295
+ function hoistedVarSiteBase(
296
+ source: ts.SourceFile,
297
+ node: ts.Node,
298
+ ): ReturnType<typeof siteBase> {
299
+ const full = lexicalScopeChain(node, source);
300
+ const scope = executionScope(full);
301
+ const scopeIndex = full.findIndex((item) => sameScope(item, scope));
302
+ return {
303
+ node,
304
+ startOffset: node.getStart(source),
305
+ endOffset: node.getEnd(),
306
+ scopeChain: full.slice(0, scopeIndex + 1),
307
+ executionScope: scope,
308
+ caseClauseStartOffset: undefined,
309
+ caseClauseEndOffset: undefined,
310
+ };
311
+ }
312
+
313
+ function declarationSites(
314
+ source: ts.SourceFile,
315
+ node: ts.VariableDeclaration,
316
+ ): BindingLexicalSite[] {
317
+ const kind = declarationKind(node);
318
+ const base = kind === 'var'
319
+ ? hoistedVarSiteBase(source, node)
320
+ : siteBase(source, node);
321
+ return bindingEntries(node.name).map((entry) => {
322
+ const aliasSource = entryAlias(node, entry);
323
+ const transaction = !entry.element
324
+ && transactionSource(node.initializer) === aliasSource;
325
+ return {
326
+ ...base,
327
+ variableName: entry.variableName,
328
+ flow: 'declaration',
329
+ declarationKind: kind,
330
+ aliasSource,
331
+ aliasKind: aliasSource
332
+ ? entry.arrayIndex === undefined
333
+ ? transaction ? 'transaction' : 'identity'
334
+ : 'array-destructuring'
335
+ : undefined,
336
+ aliasArrayIndex: entry.arrayIndex,
337
+ aliasPromiseAll: entry.arrayIndex === undefined
338
+ ? undefined
339
+ : arrayContainer(node.initializer)?.promiseAll,
340
+ deterministic: declarationKind(node) !== 'var',
341
+ };
342
+ });
343
+ }
344
+
345
+ function assignmentAlias(
346
+ node: ts.BinaryExpression,
347
+ entry: BindingAssignmentEntry,
348
+ ): string | undefined {
349
+ if (entry.unsupported) return undefined;
350
+ if (entry.arrayIndex === undefined) return directAlias(node.right);
351
+ const expression = arrayContainer(node.right)?.array.elements[entry.arrayIndex];
352
+ return expression && !ts.isOmittedExpression(expression)
353
+ ? directAlias(expression)
354
+ : undefined;
355
+ }
356
+
357
+ function assignmentSites(
358
+ source: ts.SourceFile,
359
+ node: ts.BinaryExpression,
360
+ ): BindingLexicalSite[] {
361
+ const base = siteBase(source, node);
362
+ return bindingAssignmentEntries(node.left).map((entry) => {
363
+ const aliasSource = assignmentAlias(node, entry);
364
+ const transaction = entry.arrayIndex === undefined
365
+ && transactionSource(node.right) === aliasSource;
366
+ return {
367
+ ...base,
368
+ variableName: entry.variableName,
369
+ flow: 'assignment',
370
+ aliasSource,
371
+ aliasKind: aliasSource
372
+ ? entry.arrayIndex === undefined
373
+ ? transaction ? 'transaction' : 'identity-assignment'
374
+ : 'array-destructuring'
375
+ : undefined,
376
+ aliasArrayIndex: entry.arrayIndex,
377
+ aliasPromiseAll: entry.arrayIndex === undefined
378
+ ? undefined
379
+ : arrayContainer(node.right)?.promiseAll,
380
+ deterministic: !entry.unsupported
381
+ && !shortCircuitOperators.has(node.operatorToken.kind)
382
+ && !branchDependent(node),
383
+ };
384
+ });
385
+ }
386
+
387
+ function isAssignmentOperator(kind: ts.SyntaxKind): boolean {
388
+ return kind >= ts.SyntaxKind.FirstAssignment
389
+ && kind <= ts.SyntaxKind.LastAssignment;
390
+ }
391
+
392
+ function isMutationUnary(
393
+ node: ts.PrefixUnaryExpression | ts.PostfixUnaryExpression,
394
+ ): boolean {
395
+ return node.operator === ts.SyntaxKind.PlusPlusToken
396
+ || node.operator === ts.SyntaxKind.MinusMinusToken;
397
+ }
398
+
399
+ function parameterSites(
400
+ source: ts.SourceFile,
401
+ node: ts.ParameterDeclaration,
402
+ ): BindingLexicalSite[] {
403
+ const base = siteBase(source, node.parent);
404
+ return bindingEntries(node.name).map(({ variableName }) => ({
405
+ ...base,
406
+ node,
407
+ startOffset: node.getStart(source),
408
+ endOffset: node.getEnd(),
409
+ variableName,
410
+ flow: 'declaration',
411
+ declarationKind: 'parameter',
412
+ deterministic: true,
413
+ }));
414
+ }
415
+
416
+ function shadowSite(
417
+ source: ts.SourceFile,
418
+ name: ts.Identifier,
419
+ scopeNode: ts.Node,
420
+ ): BindingLexicalSite {
421
+ const base = siteBase(source, scopeNode);
422
+ return {
423
+ ...base,
424
+ node: name,
425
+ startOffset: name.getStart(source),
426
+ endOffset: name.getEnd(),
427
+ variableName: name.text,
428
+ flow: 'shadow',
429
+ declarationKind: 'value_shadow',
430
+ deterministic: false,
431
+ };
432
+ }
433
+
434
+ function declarationShadowName(node: ts.Node): ts.Identifier | undefined {
435
+ if (ts.isFunctionDeclaration(node)) return node.name;
436
+ if (ts.isClassDeclaration(node)) return node.name;
437
+ return ts.isEnumDeclaration(node) ? node.name : undefined;
438
+ }
439
+
440
+ function expressionShadowName(node: ts.Node): ts.Identifier | undefined {
441
+ if (ts.isFunctionExpression(node)) return node.name;
442
+ return ts.isClassExpression(node) ? node.name : undefined;
443
+ }
444
+
445
+ function valueShadowSites(
446
+ source: ts.SourceFile,
447
+ node: ts.Node,
448
+ ): BindingLexicalSite[] {
449
+ const declaration = declarationShadowName(node);
450
+ if (declaration)
451
+ return [shadowSite(source, declaration, node.parent)];
452
+ if (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name))
453
+ return [shadowSite(source, node.name, node.parent)];
454
+ if (ts.isImportEqualsDeclaration(node))
455
+ return [shadowSite(source, node.name, node.parent)];
456
+ const expression = expressionShadowName(node);
457
+ if (expression) return [shadowSite(source, expression, node)];
458
+ return [];
459
+ }
460
+
461
+ function mutationSite(
462
+ source: ts.SourceFile,
463
+ expression: ts.Expression,
464
+ ): BindingLexicalSite[] {
465
+ if (!ts.isIdentifier(expression)) return [];
466
+ return [{
467
+ ...siteBase(source, expression),
468
+ variableName: expression.text,
469
+ flow: 'assignment',
470
+ deterministic: false,
471
+ }];
472
+ }
473
+
474
+ function loopWriteSites(
475
+ source: ts.SourceFile,
476
+ node: ts.ForInOrOfStatement,
477
+ ): BindingLexicalSite[] {
478
+ return ts.isVariableDeclarationList(node.initializer)
479
+ ? []
480
+ : bindingAssignmentEntries(node.initializer).map((entry) => ({
481
+ ...siteBase(source, node.initializer),
482
+ variableName: entry.variableName,
483
+ flow: 'assignment',
484
+ deterministic: false,
485
+ }));
486
+ }
487
+
488
+ function collectSites(source: ts.SourceFile): BindingLexicalSite[] {
489
+ const sites: BindingLexicalSite[] = [];
490
+ const visit = (node: ts.Node): void => {
491
+ sites.push(...valueShadowSites(source, node));
492
+ if (ts.isVariableDeclaration(node))
493
+ sites.push(...declarationSites(source, node));
494
+ if (ts.isParameter(node))
495
+ sites.push(...parameterSites(source, node));
496
+ if (ts.isBinaryExpression(node)
497
+ && isAssignmentOperator(node.operatorToken.kind))
498
+ sites.push(...assignmentSites(source, node));
499
+ if (ts.isForInStatement(node) || ts.isForOfStatement(node))
500
+ sites.push(...loopWriteSites(source, node));
501
+ if ((ts.isPrefixUnaryExpression(node)
502
+ || ts.isPostfixUnaryExpression(node)) && isMutationUnary(node))
503
+ sites.push(...mutationSite(source, node.operand));
504
+ ts.forEachChild(node, visit);
505
+ };
506
+ visit(source);
507
+ return sites.sort((left, right) =>
508
+ left.startOffset - right.startOffset || left.endOffset - right.endOffset);
509
+ }
510
+
511
+ function declarationKey(site: BindingLexicalSite): string {
512
+ return [
513
+ site.variableName,
514
+ site.startOffset,
515
+ site.endOffset,
516
+ site.declarationKind,
517
+ ].join('\u0000');
518
+ }
519
+
520
+ export function declarationAt(
521
+ sites: readonly BindingLexicalSite[],
522
+ variableName: string,
523
+ useStart: number,
524
+ useChain: readonly LexicalScopeFact[],
525
+ ): { site?: BindingLexicalSite; scopeIndex?: number; after: boolean; ambiguous: boolean } {
526
+ const declarations = sites.flatMap((site) => {
527
+ if (!['declaration', 'shadow'].includes(site.flow)
528
+ || site.variableName !== variableName) return [];
529
+ const scope = site.scopeChain.at(-1);
530
+ const scopeIndex = scope
531
+ ? useChain.findIndex((candidate) => sameScope(candidate, scope))
532
+ : -1;
533
+ return scopeIndex >= 0 ? [{ site, scopeIndex }] : [];
534
+ });
535
+ const deepest = Math.max(-1, ...declarations.map((item) => item.scopeIndex));
536
+ const nearest = declarations.filter((item) => item.scopeIndex === deepest);
537
+ if (nearest.length === 0) return { after: false, ambiguous: false };
538
+ if (nearest.length > 1)
539
+ return { after: false, ambiguous: true, scopeIndex: deepest };
540
+ const site = nearest[0]?.site;
541
+ return {
542
+ site,
543
+ scopeIndex: deepest,
544
+ after: Boolean(site && site.declarationKind !== 'var'
545
+ && site.startOffset >= useStart),
546
+ ambiguous: false,
547
+ };
548
+ }
549
+
550
+ function attachDeclarationKeys(sites: BindingLexicalSite[]): void {
551
+ for (const site of sites) {
552
+ if (site.flow === 'declaration' || site.flow === 'shadow') {
553
+ site.declarationKey = declarationKey(site);
554
+ continue;
555
+ }
556
+ const selected = declarationAt(
557
+ sites, site.variableName, site.startOffset, site.scopeChain,
558
+ );
559
+ if (selected.site && !selected.after && !selected.ambiguous)
560
+ site.declarationKey = declarationKey(selected.site);
561
+ }
562
+ }
563
+
564
+ export function createBindingLexicalIndex(
565
+ source: ts.SourceFile,
566
+ ): BindingLexicalIndex {
567
+ const sites = collectSites(source);
568
+ attachDeclarationKeys(sites);
569
+ return { source, sites };
570
+ }
571
+
572
+ export function bindingSite(
573
+ index: BindingLexicalIndex,
574
+ variableName: string,
575
+ startOffset: number,
576
+ endOffset: number,
577
+ ): BindingLexicalSite | undefined {
578
+ const matches = index.sites.filter((site) =>
579
+ site.variableName === variableName
580
+ && site.startOffset === startOffset
581
+ && site.endOffset === endOffset);
582
+ return matches.length === 1 ? matches[0] : undefined;
583
+ }