@saptools/service-flow 0.1.58 → 0.1.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  stripQuotes,
18
18
  substituteVariables,
19
19
  trace
20
- } from "./chunk-CRSSXWQ2.js";
20
+ } from "./chunk-GLZSDBRG.js";
21
21
 
22
22
  // src/parsers/generated-constants-parser.ts
23
23
  import fs from "fs/promises";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saptools/service-flow",
3
- "version": "0.1.58",
3
+ "version": "0.1.59",
4
4
  "description": "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -4,6 +4,8 @@ const capQueryBuilderRoots = new Set([
4
4
  'SELECT.from',
5
5
  'SELECT.one.from',
6
6
  'SELECT.one',
7
+ 'SELECT.distinct.from',
8
+ 'SELECT.distinct.one.from',
7
9
  'INSERT.into',
8
10
  'UPSERT.into',
9
11
  'UPDATE.entity',
@@ -0,0 +1,622 @@
1
+ import ts from 'typescript';
2
+ import { isCapQueryBuilderRootName } from './000-direct-query-execution.js';
3
+
4
+ export interface BindingResolution {
5
+ declaration?: ts.VariableDeclaration | ts.ParameterDeclaration;
6
+ initializer?: ts.Expression;
7
+ immutable: boolean;
8
+ evidence: string[];
9
+ }
10
+
11
+ interface DestructuredBinding {
12
+ declaration: ts.VariableDeclaration | ts.ParameterDeclaration;
13
+ initializer?: ts.Expression;
14
+ entityName: string;
15
+ }
16
+
17
+ export const maxAliasDepth = 5;
18
+ const cdsModelSpecifier = /^#cds-models(?:\/|$)/;
19
+ const modelNamespaceCache = new WeakMap<ts.SourceFile, Set<string>>();
20
+
21
+ export function expressionName(expr: ts.Expression): string {
22
+ if (ts.isIdentifier(expr)) return expr.text;
23
+ if (ts.isPropertyAccessExpression(expr))
24
+ return `${expressionName(expr.expression)}.${expr.name.text}`;
25
+ return expr.getText();
26
+ }
27
+
28
+ export function variableInitializers(
29
+ source: ts.SourceFile,
30
+ ): Map<string, ts.Expression> {
31
+ const initializers = new Map<string, ts.Expression>();
32
+ for (const statement of source.statements) {
33
+ if (!ts.isVariableStatement(statement)
34
+ || (statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
35
+ for (const declaration of statement.declarationList.declarations) {
36
+ if (ts.isIdentifier(declaration.name) && declaration.initializer)
37
+ initializers.set(declaration.name.text, declaration.initializer);
38
+ }
39
+ }
40
+ return initializers;
41
+ }
42
+
43
+ function unwrapQueryExpression(expr: ts.Expression): ts.Expression {
44
+ if (ts.isParenthesizedExpression(expr) || ts.isAwaitExpression(expr)
45
+ || ts.isAsExpression(expr) || ts.isTypeAssertionExpression(expr)
46
+ || ts.isNonNullExpression(expr) || ts.isSatisfiesExpression(expr))
47
+ return unwrapQueryExpression(expr.expression);
48
+ return expr;
49
+ }
50
+
51
+ function isFunctionLikeScope(node: ts.Node): boolean {
52
+ return ts.isFunctionLike(node) || ts.isSourceFile(node);
53
+ }
54
+
55
+ function nodeContains(parent: ts.Node, child: ts.Node): boolean {
56
+ const source = child.getSourceFile();
57
+ return child.getStart(source) >= parent.getStart(source)
58
+ && child.getEnd() <= parent.getEnd();
59
+ }
60
+
61
+ function isLoopInitializerScope(
62
+ declaration: ts.VariableDeclaration,
63
+ scope: ts.Node,
64
+ ): boolean {
65
+ const list = declaration.parent;
66
+ return (ts.isForStatement(scope) && scope.initializer === list)
67
+ || ((ts.isForInStatement(scope) || ts.isForOfStatement(scope))
68
+ && scope.initializer === list);
69
+ }
70
+
71
+ function isLexicalScopeBoundary(
72
+ node: ts.Node,
73
+ declaration: ts.VariableDeclaration,
74
+ ): boolean {
75
+ return ts.isBlock(node) || ts.isSourceFile(node) || ts.isModuleBlock(node)
76
+ || ts.isCaseBlock(node) || isLoopInitializerScope(declaration, node)
77
+ || isFunctionLikeScope(node);
78
+ }
79
+
80
+ function declarationScope(
81
+ node: ts.VariableDeclaration | ts.ParameterDeclaration,
82
+ ): ts.Node {
83
+ if (ts.isParameter(node)) return node.parent;
84
+ if (ts.isCatchClause(node.parent) && node.parent.variableDeclaration === node)
85
+ return node.parent;
86
+ const list = node.parent;
87
+ const blockScoped = (list.flags & (ts.NodeFlags.Const | ts.NodeFlags.Let)) !== 0;
88
+ let current: ts.Node = list.parent;
89
+ if (!blockScoped) {
90
+ while (current.parent && !isFunctionLikeScope(current)) current = current.parent;
91
+ return current;
92
+ }
93
+ while (current.parent && !isLexicalScopeBoundary(current, node))
94
+ current = current.parent;
95
+ return current;
96
+ }
97
+
98
+ function catchBindingScope(
99
+ declaration: ts.VariableDeclaration | ts.ParameterDeclaration,
100
+ ): ts.CatchClause | undefined {
101
+ if (ts.isParameter(declaration)) return undefined;
102
+ return ts.isCatchClause(declaration.parent)
103
+ && declaration.parent.variableDeclaration === declaration
104
+ ? declaration.parent
105
+ : undefined;
106
+ }
107
+
108
+ function declarationIsInScope(
109
+ declaration: ts.VariableDeclaration | ts.ParameterDeclaration,
110
+ use: ts.Node,
111
+ ): boolean {
112
+ const catchScope = catchBindingScope(declaration);
113
+ if (catchScope) return nodeContains(catchScope.block, use);
114
+ const scope = declarationScope(declaration);
115
+ if (ts.isForStatement(scope) || ts.isForInStatement(scope)
116
+ || ts.isForOfStatement(scope)) return nodeContains(scope.statement, use);
117
+ return ts.isSourceFile(scope) || nodeContains(scope, use);
118
+ }
119
+
120
+ function isAccessibleDeclaration(
121
+ declaration: ts.VariableDeclaration | ts.ParameterDeclaration,
122
+ use: ts.Node,
123
+ ): boolean {
124
+ return declaration.name.getStart(use.getSourceFile()) < use.getStart()
125
+ && declarationIsInScope(declaration, use);
126
+ }
127
+
128
+ export function resolveBinding(
129
+ identifier: ts.Identifier,
130
+ use: ts.Node,
131
+ ): BindingResolution {
132
+ const source = use.getSourceFile();
133
+ let best: ts.VariableDeclaration | ts.ParameterDeclaration | undefined;
134
+ const visit = (node: ts.Node): void => {
135
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)
136
+ && node.name.text === identifier.text && isAccessibleDeclaration(node, use))
137
+ best = node;
138
+ if (ts.isParameter(node) && ts.isIdentifier(node.name)
139
+ && node.name.text === identifier.text && isAccessibleDeclaration(node, use))
140
+ best = node;
141
+ ts.forEachChild(node, visit);
142
+ };
143
+ visit(source);
144
+ if (!best) return { immutable: false, evidence: ['binding_not_found'] };
145
+ const immutable = ts.isVariableDeclaration(best)
146
+ && (best.parent.flags & ts.NodeFlags.Const) !== 0;
147
+ return {
148
+ declaration: best,
149
+ initializer: ts.isVariableDeclaration(best) ? best.initializer : undefined,
150
+ immutable,
151
+ evidence: [immutable
152
+ ? 'lexical_const_binding_before_use'
153
+ : 'lexical_mutable_or_parameter_binding'],
154
+ };
155
+ }
156
+
157
+ function directBindingElement(
158
+ name: ts.BindingName,
159
+ identifier: ts.Identifier,
160
+ ): ts.BindingElement | undefined {
161
+ if (ts.isIdentifier(name)) return undefined;
162
+ for (const element of name.elements) {
163
+ if (ts.isOmittedExpression(element) || element.dotDotDotToken
164
+ || element.initializer
165
+ || !ts.isIdentifier(element.name)) continue;
166
+ if (element.name.text === identifier.text) return element;
167
+ }
168
+ return undefined;
169
+ }
170
+
171
+ function bindingNameContains(name: ts.BindingName, target: string): boolean {
172
+ if (ts.isIdentifier(name)) return name.text === target;
173
+ return name.elements.some((element) => ts.isBindingElement(element)
174
+ && bindingNameContains(element.name, target));
175
+ }
176
+
177
+ function bindingScope(node: ts.Node): ts.Node {
178
+ if (ts.isVariableDeclaration(node) || ts.isParameter(node))
179
+ return declarationScope(node);
180
+ if (ts.isFunctionExpression(node) || ts.isClassExpression(node)) return node;
181
+ return node.parent;
182
+ }
183
+
184
+ function bindingIsCloser(candidate: ts.Node, current: ts.Node, use: ts.Node): boolean {
185
+ const source = use.getSourceFile();
186
+ const candidateScope = bindingScope(candidate);
187
+ const currentScope = bindingScope(current);
188
+ const candidateSpan = candidateScope.getEnd() - candidateScope.getStart(source);
189
+ const currentSpan = currentScope.getEnd() - currentScope.getStart(source);
190
+ return candidateSpan < currentSpan
191
+ || (candidateSpan === currentSpan
192
+ && candidate.getStart(source) > current.getStart(source));
193
+ }
194
+
195
+ function nearestScopedVariableDeclaration(
196
+ identifier: ts.Identifier,
197
+ ): ts.VariableDeclaration | ts.ParameterDeclaration | undefined {
198
+ let best: ts.VariableDeclaration | ts.ParameterDeclaration | undefined;
199
+ const visit = (node: ts.Node): void => {
200
+ if ((ts.isVariableDeclaration(node) || ts.isParameter(node))
201
+ && bindingNameContains(node.name, identifier.text)
202
+ && declarationIsInScope(node, identifier)
203
+ && (!best || bindingIsCloser(node, best, identifier))) best = node;
204
+ ts.forEachChild(node, visit);
205
+ };
206
+ visit(identifier.getSourceFile());
207
+ return best;
208
+ }
209
+
210
+ function hasScopedVariableDeclaration(identifier: ts.Identifier): boolean {
211
+ return Boolean(nearestScopedVariableDeclaration(identifier));
212
+ }
213
+
214
+ function namedValueDeclarationMatches(
215
+ node: ts.Node,
216
+ name: string,
217
+ ): boolean {
218
+ if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)
219
+ || ts.isEnumDeclaration(node)) return node.name?.text === name;
220
+ return ts.isModuleDeclaration(node) && ts.isIdentifier(node.name)
221
+ && node.name.text === name;
222
+ }
223
+
224
+ function declarationContainerContains(node: ts.Node, use: ts.Node): boolean {
225
+ const container = node.parent;
226
+ return ts.isSourceFile(container)
227
+ || ((ts.isBlock(container) || ts.isModuleBlock(container))
228
+ && nodeContains(container, use));
229
+ }
230
+
231
+ function selfNamedValueDeclarationMatches(
232
+ node: ts.Node,
233
+ identifier: ts.Identifier,
234
+ ): boolean {
235
+ return (ts.isFunctionExpression(node) || ts.isClassExpression(node))
236
+ && node.name?.text === identifier.text
237
+ && nodeContains(node, identifier);
238
+ }
239
+
240
+ function scopedNonVariableDeclarationMatches(
241
+ node: ts.Node,
242
+ identifier: ts.Identifier,
243
+ ): boolean {
244
+ return (namedValueDeclarationMatches(node, identifier.text)
245
+ && declarationContainerContains(node, identifier))
246
+ || selfNamedValueDeclarationMatches(node, identifier);
247
+ }
248
+
249
+ function nearestScopedNonVariableDeclaration(
250
+ identifier: ts.Identifier,
251
+ ): ts.Node | undefined {
252
+ let best: ts.Node | undefined;
253
+ const visit = (node: ts.Node): void => {
254
+ if (scopedNonVariableDeclarationMatches(node, identifier)
255
+ && (!best || bindingIsCloser(node, best, identifier))) best = node;
256
+ ts.forEachChild(node, visit);
257
+ };
258
+ visit(identifier.getSourceFile());
259
+ return best;
260
+ }
261
+
262
+ function hasScopedNonVariableDeclaration(identifier: ts.Identifier): boolean {
263
+ return Boolean(nearestScopedNonVariableDeclaration(identifier));
264
+ }
265
+
266
+ function nonVariableBindingWins(
267
+ identifier: ts.Identifier,
268
+ selected: ts.Node | undefined,
269
+ ): boolean {
270
+ const declaration = nearestScopedNonVariableDeclaration(identifier);
271
+ return Boolean(declaration
272
+ && (!selected || bindingIsCloser(declaration, selected, identifier)));
273
+ }
274
+
275
+ function importClauseBinds(
276
+ clause: ts.ImportClause | undefined,
277
+ name: string,
278
+ ): boolean {
279
+ if (!clause || clause.isTypeOnly) return false;
280
+ if (clause.name?.text === name) return true;
281
+ const bindings = clause.namedBindings;
282
+ if (!bindings) return false;
283
+ if (ts.isNamespaceImport(bindings)) return bindings.name.text === name;
284
+ return bindings.elements.some((element) => !element.isTypeOnly
285
+ && element.name.text === name);
286
+ }
287
+
288
+ function hasValueImportBinding(identifier: ts.Identifier): boolean {
289
+ return identifier.getSourceFile().statements.some((statement) => {
290
+ if (ts.isImportDeclaration(statement))
291
+ return importClauseBinds(statement.importClause, identifier.text);
292
+ return ts.isImportEqualsDeclaration(statement) && !statement.isTypeOnly
293
+ && statement.name.text === identifier.text;
294
+ });
295
+ }
296
+
297
+ function sourcePropertyName(
298
+ element: ts.BindingElement,
299
+ localName: string,
300
+ ): string | undefined {
301
+ const property = element.propertyName;
302
+ if (!property) return localName;
303
+ if (ts.isIdentifier(property) || ts.isStringLiteral(property)
304
+ || ts.isNumericLiteral(property)) return property.text;
305
+ return undefined;
306
+ }
307
+
308
+ function resolveDestructuredBinding(
309
+ identifier: ts.Identifier,
310
+ use: ts.Node,
311
+ ): DestructuredBinding | undefined {
312
+ const source = use.getSourceFile();
313
+ let best: DestructuredBinding | undefined;
314
+ const visit = (node: ts.Node): void => {
315
+ if ((ts.isVariableDeclaration(node) || ts.isParameter(node))
316
+ && isAccessibleDeclaration(node, use)) {
317
+ const element = directBindingElement(node.name, identifier);
318
+ const entityName = element
319
+ ? sourcePropertyName(element, identifier.text)
320
+ : undefined;
321
+ if (entityName && (!best || node.name.getStart(source)
322
+ > best.declaration.name.getStart(source))) {
323
+ best = {
324
+ declaration: node,
325
+ initializer: node.initializer,
326
+ entityName,
327
+ };
328
+ }
329
+ }
330
+ ts.forEachChild(node, visit);
331
+ };
332
+ visit(source);
333
+ return best;
334
+ }
335
+
336
+ function isEntitiesBase(expr: ts.Expression): boolean {
337
+ const value = unwrapQueryExpression(expr);
338
+ if (ts.isPropertyAccessExpression(value)) return value.name.text === 'entities';
339
+ return ts.isCallExpression(value)
340
+ && ts.isPropertyAccessExpression(value.expression)
341
+ && value.expression.name.text === 'entities';
342
+ }
343
+
344
+ function hasLexicalValueShadow(
345
+ identifier: ts.Identifier,
346
+ includeImports: boolean,
347
+ ): boolean {
348
+ return hasScopedVariableDeclaration(identifier)
349
+ || hasScopedNonVariableDeclaration(identifier)
350
+ || (includeImports && hasValueImportBinding(identifier));
351
+ }
352
+
353
+ function modelRequire(expr: ts.Expression): boolean {
354
+ const value = unwrapQueryExpression(expr);
355
+ if (!ts.isCallExpression(value) || !ts.isIdentifier(value.expression)
356
+ || value.expression.text !== 'require' || value.arguments.length !== 1)
357
+ return false;
358
+ if (hasLexicalValueShadow(value.expression, true)) return false;
359
+ const specifier = value.arguments[0];
360
+ return Boolean(specifier && ts.isStringLiteralLike(specifier)
361
+ && cdsModelSpecifier.test(specifier.text));
362
+ }
363
+
364
+ function modelNamespaceImportName(
365
+ statement: ts.Statement,
366
+ ): string | undefined {
367
+ if (!ts.isImportDeclaration(statement)
368
+ || !ts.isStringLiteralLike(statement.moduleSpecifier)
369
+ || !cdsModelSpecifier.test(statement.moduleSpecifier.text)
370
+ || statement.importClause?.isTypeOnly) return undefined;
371
+ const bindings = statement.importClause?.namedBindings;
372
+ return bindings && ts.isNamespaceImport(bindings)
373
+ ? bindings.name.text
374
+ : undefined;
375
+ }
376
+
377
+ function modelImportEqualsName(
378
+ statement: ts.Statement,
379
+ ): string | undefined {
380
+ if (!ts.isImportEqualsDeclaration(statement) || statement.isTypeOnly
381
+ || !ts.isExternalModuleReference(statement.moduleReference)) return undefined;
382
+ const specifier = statement.moduleReference.expression;
383
+ return specifier && ts.isStringLiteralLike(specifier)
384
+ && cdsModelSpecifier.test(specifier.text)
385
+ ? statement.name.text
386
+ : undefined;
387
+ }
388
+
389
+ function modelNamespaceNames(source: ts.SourceFile): Set<string> {
390
+ const cached = modelNamespaceCache.get(source);
391
+ if (cached) return cached;
392
+ const names = new Set<string>();
393
+ for (const statement of source.statements) {
394
+ const name = modelNamespaceImportName(statement)
395
+ ?? modelImportEqualsName(statement);
396
+ if (name) names.add(name);
397
+ }
398
+ modelNamespaceCache.set(source, names);
399
+ return names;
400
+ }
401
+
402
+ function destructuredEntitySource(
403
+ initializer: ts.Expression | undefined,
404
+ ): boolean {
405
+ if (!initializer) return false;
406
+ if (isEntitiesBase(initializer) || modelRequire(initializer)) return true;
407
+ const value = unwrapQueryExpression(initializer);
408
+ return ts.isIdentifier(value)
409
+ && !hasLexicalValueShadow(value, false)
410
+ && modelNamespaceNames(value.getSourceFile()).has(value.text);
411
+ }
412
+
413
+ function isAssignmentOperator(kind: ts.SyntaxKind): boolean {
414
+ return kind >= ts.SyntaxKind.FirstAssignment
415
+ && kind <= ts.SyntaxKind.LastAssignment;
416
+ }
417
+
418
+ function identifierIsReadWithinTarget(
419
+ identifier: ts.Identifier,
420
+ parent: ts.Node,
421
+ ): boolean {
422
+ if (ts.isPropertyAccessExpression(parent) && parent.name === identifier) return true;
423
+ if (ts.isElementAccessExpression(parent) && parent.argumentExpression
424
+ && nodeContains(parent.argumentExpression, identifier)) return true;
425
+ if (ts.isPropertyAssignment(parent) && nodeContains(parent.name, identifier)) return true;
426
+ return ts.isComputedPropertyName(parent) && nodeContains(parent.expression, identifier);
427
+ }
428
+
429
+ function identifierIsUnaryWrite(identifier: ts.Identifier): boolean {
430
+ const direct = identifier.parent;
431
+ if (!((ts.isPrefixUnaryExpression(direct) || ts.isPostfixUnaryExpression(direct))
432
+ && direct.operand === identifier)) return false;
433
+ return direct.operator === ts.SyntaxKind.PlusPlusToken
434
+ || direct.operator === ts.SyntaxKind.MinusMinusToken;
435
+ }
436
+
437
+ function identifierIsNestedWrite(identifier: ts.Identifier): boolean {
438
+ let current: ts.Node = identifier;
439
+ while (current.parent) {
440
+ const parent = current.parent;
441
+ if (identifierIsReadWithinTarget(identifier, parent)) return false;
442
+ if (ts.isBinaryExpression(parent))
443
+ return isAssignmentOperator(parent.operatorToken.kind)
444
+ && nodeContains(parent.left, identifier);
445
+ if (ts.isForInStatement(parent) || ts.isForOfStatement(parent))
446
+ return nodeContains(parent.initializer, identifier);
447
+ current = parent;
448
+ }
449
+ return false;
450
+ }
451
+
452
+ function identifierIsWrite(identifier: ts.Identifier): boolean {
453
+ return identifierIsUnaryWrite(identifier) || identifierIsNestedWrite(identifier);
454
+ }
455
+
456
+ function destructuredBindingWasWritten(
457
+ binding: DestructuredBinding,
458
+ localName: string,
459
+ use: ts.Node,
460
+ ): boolean {
461
+ if (ts.isParameter(binding.declaration)) return true;
462
+ if ((binding.declaration.parent.flags & ts.NodeFlags.Const) !== 0) return false;
463
+ let written = false;
464
+ const declarationEnd = binding.declaration.name.getEnd();
465
+ const useStart = use.getStart();
466
+ const visit = (node: ts.Node): void => {
467
+ if (written) return;
468
+ if (ts.isIdentifier(node) && node.text === localName
469
+ && node.getStart() > declarationEnd && node.getStart() < useStart
470
+ && identifierIsWrite(node)
471
+ && resolveDestructuredBinding(node, node)?.declaration === binding.declaration)
472
+ written = true;
473
+ ts.forEachChild(node, visit);
474
+ };
475
+ visit(use.getSourceFile());
476
+ return written;
477
+ }
478
+
479
+ function destructuredBindingWins(
480
+ destructured: DestructuredBinding,
481
+ simple: BindingResolution,
482
+ use: ts.Node,
483
+ ): boolean {
484
+ return !simple.declaration
485
+ || bindingIsCloser(destructured.declaration, simple.declaration, use);
486
+ }
487
+
488
+ function entityFromDestructuredBinding(
489
+ binding: DestructuredBinding,
490
+ identifier: ts.Identifier,
491
+ ): string | undefined {
492
+ if (destructuredBindingWasWritten(binding, identifier.text, identifier))
493
+ return undefined;
494
+ return destructuredEntitySource(binding.initializer)
495
+ ? binding.entityName
496
+ : undefined;
497
+ }
498
+
499
+ function entityFromSimpleBinding(
500
+ binding: BindingResolution,
501
+ depth: number,
502
+ seen: Set<ts.Node>,
503
+ ): string | undefined {
504
+ if (!binding.declaration || !binding.immutable || !binding.initializer
505
+ || seen.has(binding.declaration)) return undefined;
506
+ seen.add(binding.declaration);
507
+ return entityFromExpression(binding.initializer, depth + 1, seen);
508
+ }
509
+
510
+ function scopedBindingIsCurrent(
511
+ identifier: ts.Identifier,
512
+ declaration: ts.VariableDeclaration | ts.ParameterDeclaration,
513
+ ): boolean {
514
+ return nearestScopedVariableDeclaration(identifier) === declaration
515
+ && !nonVariableBindingWins(identifier, declaration);
516
+ }
517
+
518
+ function unboundEntityName(identifier: ts.Identifier): string | undefined {
519
+ return hasScopedVariableDeclaration(identifier)
520
+ || hasScopedNonVariableDeclaration(identifier)
521
+ ? undefined
522
+ : identifier.text;
523
+ }
524
+
525
+ function entityFromIdentifier(
526
+ expr: ts.Identifier,
527
+ depth: number,
528
+ seen: Set<ts.Node>,
529
+ ): string | undefined {
530
+ const binding = resolveBinding(expr, expr);
531
+ const destructured = resolveDestructuredBinding(expr, expr);
532
+ if (destructured && destructuredBindingWins(destructured, binding, expr)) {
533
+ return scopedBindingIsCurrent(expr, destructured.declaration)
534
+ ? entityFromDestructuredBinding(destructured, expr)
535
+ : undefined;
536
+ }
537
+ if (!binding.declaration) return unboundEntityName(expr);
538
+ if (!scopedBindingIsCurrent(expr, binding.declaration)) return undefined;
539
+ return entityFromSimpleBinding(binding, depth, seen);
540
+ }
541
+
542
+ function literalEntity(expr: ts.Expression): string | undefined {
543
+ if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr))
544
+ return expr.text;
545
+ if (!ts.isElementAccessExpression(expr) || !expr.argumentExpression)
546
+ return undefined;
547
+ return ts.isStringLiteral(expr.argumentExpression)
548
+ || ts.isNoSubstitutionTemplateLiteral(expr.argumentExpression)
549
+ ? expr.argumentExpression.text
550
+ : undefined;
551
+ }
552
+
553
+ function propertyEntity(expr: ts.Expression): string | undefined {
554
+ if (!ts.isPropertyAccessExpression(expr)) return undefined;
555
+ if (expr.expression.kind === ts.SyntaxKind.ThisKeyword) return undefined;
556
+ return isEntitiesBase(expr.expression) ? expr.name.text : undefined;
557
+ }
558
+
559
+ function entityFromExpression(
560
+ expr: ts.Expression | undefined,
561
+ depth = 0,
562
+ seen = new Set<ts.Node>(),
563
+ ): string | undefined {
564
+ if (!expr || depth >= maxAliasDepth) return undefined;
565
+ const value = unwrapQueryExpression(expr);
566
+ const literal = literalEntity(value);
567
+ if (literal !== undefined) return literal;
568
+ if (ts.isIdentifier(value)) return entityFromIdentifier(value, depth, seen);
569
+ return propertyEntity(value);
570
+ }
571
+
572
+ function queryAliasInitializer(
573
+ identifier: ts.Identifier,
574
+ initializers: Map<string, ts.Expression>,
575
+ seen: Set<string>,
576
+ ): ts.Expression | undefined {
577
+ const initializer = initializers.get(identifier.text);
578
+ if (!initializer || seen.has(identifier.text)) return undefined;
579
+ const binding = resolveBinding(identifier, identifier);
580
+ if (!binding.declaration || !binding.immutable || binding.initializer !== initializer
581
+ || nearestScopedVariableDeclaration(identifier) !== binding.declaration
582
+ || nonVariableBindingWins(identifier, binding.declaration)) return undefined;
583
+ seen.add(identifier.text);
584
+ return initializer;
585
+ }
586
+
587
+ function queryEntityFromCall(
588
+ call: ts.CallExpression,
589
+ initializers: Map<string, ts.Expression>,
590
+ seenInitializers: Set<string>,
591
+ ): string | undefined {
592
+ const name = expressionName(call.expression);
593
+ if (name === 'cds.run')
594
+ return queryEntityFromAst(call.arguments[0], initializers, seenInitializers);
595
+ if (isCapQueryBuilderRootName(name))
596
+ return entityFromExpression(call.arguments[0]);
597
+ const receiver = ts.isPropertyAccessExpression(call.expression)
598
+ ? call.expression.expression
599
+ : undefined;
600
+ return receiver
601
+ ? queryEntityFromAst(receiver, initializers, seenInitializers)
602
+ : undefined;
603
+ }
604
+
605
+ export function queryEntityFromAst(
606
+ expr: ts.Expression,
607
+ initializers = new Map<string, ts.Expression>(),
608
+ seenInitializers = new Set<string>(),
609
+ ): string | undefined {
610
+ const unwrapped = unwrapQueryExpression(expr);
611
+ if (ts.isIdentifier(unwrapped)) {
612
+ const initializer = queryAliasInitializer(
613
+ unwrapped, initializers, seenInitializers,
614
+ );
615
+ return initializer
616
+ ? queryEntityFromAst(initializer, initializers, seenInitializers)
617
+ : undefined;
618
+ }
619
+ return ts.isCallExpression(unwrapped)
620
+ ? queryEntityFromCall(unwrapped, initializers, seenInitializers)
621
+ : undefined;
622
+ }