@saptools/service-flow 0.1.67 → 0.1.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +20 -8
  3. package/TECHNICAL-NOTE.md +27 -0
  4. package/dist/{chunk-ZQABU7MR.js → chunk-AEM4JY22.js} +12676 -6714
  5. package/dist/chunk-AEM4JY22.js.map +1 -0
  6. package/dist/cli.js +2325 -413
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +61 -2
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/003-doctor-package-resolution.ts +68 -0
  12. package/src/cli/doctor.ts +25 -14
  13. package/src/db/000-call-fact-repository.ts +475 -342
  14. package/src/db/001-fact-lifecycle.ts +60 -30
  15. package/src/db/002-fact-json-inventory.ts +169 -0
  16. package/src/db/003-current-fact-semantics.ts +698 -0
  17. package/src/db/004-package-target-invalidation.ts +173 -0
  18. package/src/db/005-schema-structure.ts +201 -0
  19. package/src/db/006-relative-symbol-resolution.ts +443 -0
  20. package/src/db/007-package-fact-semantics.ts +573 -0
  21. package/src/db/008-relative-fact-semantics.ts +207 -0
  22. package/src/db/009-binding-fact-semantics.ts +347 -0
  23. package/src/db/010-package-symbol-surface-semantics.ts +320 -0
  24. package/src/db/011-symbol-call-semantics.ts +144 -0
  25. package/src/db/012-binding-reference-proof.ts +264 -0
  26. package/src/db/migrations.ts +16 -3
  27. package/src/db/repositories.ts +113 -6
  28. package/src/db/schema.ts +4 -2
  29. package/src/index.ts +12 -0
  30. package/src/indexer/repository-indexer.ts +72 -11
  31. package/src/indexer/workspace-indexer.ts +123 -32
  32. package/src/linker/003-package-import-symbol-resolver.ts +363 -131
  33. package/src/linker/004-event-subscription-handler-linker.ts +2 -0
  34. package/src/linker/005-odata-path-structure.ts +371 -0
  35. package/src/linker/cross-repo-linker.ts +2 -1
  36. package/src/linker/odata-path-normalizer.ts +273 -180
  37. package/src/linker/service-resolver.ts +197 -77
  38. package/src/parsers/002-symbol-import-bindings.ts +516 -0
  39. package/src/parsers/003-package-public-surface.ts +661 -0
  40. package/src/parsers/004-fact-identity.ts +108 -0
  41. package/src/parsers/005-event-subscription-facts.ts +281 -0
  42. package/src/parsers/006-binding-identity.ts +343 -0
  43. package/src/parsers/007-source-fact-reconciliation.ts +105 -0
  44. package/src/parsers/008-package-surface-publication.ts +82 -0
  45. package/src/parsers/009-symbol-call-facts.ts +528 -0
  46. package/src/parsers/010-package-public-surface-analysis.ts +352 -0
  47. package/src/parsers/011-binding-lexical-scope.ts +583 -0
  48. package/src/parsers/012-package-fact-contract.ts +306 -0
  49. package/src/parsers/013-executable-body-eligibility.ts +35 -0
  50. package/src/parsers/014-service-binding-helper-flow.ts +240 -0
  51. package/src/parsers/015-service-binding-collector.ts +693 -0
  52. package/src/parsers/016-local-symbol-reference.ts +261 -0
  53. package/src/parsers/017-symbol-derived-contexts.ts +268 -0
  54. package/src/parsers/018-package-commonjs-syntax.ts +142 -0
  55. package/src/parsers/019-binding-assignment-targets.ts +76 -0
  56. package/src/parsers/020-stable-local-value.ts +217 -0
  57. package/src/parsers/021-binding-visibility.ts +152 -0
  58. package/src/parsers/operation-path-analysis.ts +6 -1
  59. package/src/parsers/outbound-call-parser.ts +19 -6
  60. package/src/parsers/package-json-parser.ts +45 -3
  61. package/src/parsers/service-binding-parser-helpers.ts +86 -15
  62. package/src/parsers/service-binding-parser.ts +147 -597
  63. package/src/parsers/symbol-parser.ts +482 -353
  64. package/src/trace/002-trace-diagnostics.ts +36 -1
  65. package/src/trace/016-compact-projector.ts +16 -17
  66. package/src/trace/020-compact-field-projection.ts +48 -37
  67. package/src/trace/021-compact-decision-normalization.ts +105 -0
  68. package/src/trace/022-trace-fact-preflight.ts +21 -0
  69. package/src/trace/023-nested-event-scopes.ts +23 -0
  70. package/src/trace/024-compact-observation-decision.ts +76 -0
  71. package/src/trace/025-trace-implementation-scope.ts +123 -0
  72. package/src/trace/026-trace-start-scope.ts +335 -0
  73. package/src/trace/027-trace-scope-execution.ts +566 -0
  74. package/src/trace/028-trace-operation-execution.ts +336 -0
  75. package/src/trace/029-trace-start-implementation.ts +172 -0
  76. package/src/trace/trace-engine.ts +122 -624
  77. package/src/types.ts +56 -0
  78. package/src/utils/001-placeholders.ts +188 -10
  79. package/src/version.ts +1 -1
  80. package/dist/chunk-ZQABU7MR.js.map +0 -1
@@ -0,0 +1,76 @@
1
+ import ts from 'typescript';
2
+
3
+ export interface BindingAssignmentEntry {
4
+ variableName: string;
5
+ arrayIndex?: number;
6
+ unsupported?: boolean;
7
+ }
8
+
9
+ function unsupported(
10
+ entries: readonly BindingAssignmentEntry[],
11
+ ): BindingAssignmentEntry[] {
12
+ return entries.map((entry) => ({
13
+ ...entry,
14
+ arrayIndex: undefined,
15
+ unsupported: true,
16
+ }));
17
+ }
18
+
19
+ function objectEntries(
20
+ property: ts.ObjectLiteralElementLike,
21
+ ): BindingAssignmentEntry[] {
22
+ if (ts.isShorthandPropertyAssignment(property))
23
+ return [{ variableName: property.name.text }];
24
+ if (ts.isSpreadAssignment(property))
25
+ return unsupported(bindingAssignmentEntries(property.expression));
26
+ if (!ts.isPropertyAssignment(property)) return [];
27
+ if (ts.isIdentifier(property.initializer))
28
+ return [{ variableName: property.initializer.text }];
29
+ return unsupported(bindingAssignmentEntries(property.initializer));
30
+ }
31
+
32
+ function arrayEntries(
33
+ element: ts.Expression | ts.OmittedExpression,
34
+ arrayIndex: number,
35
+ ): BindingAssignmentEntry[] {
36
+ if (ts.isOmittedExpression(element)) return [];
37
+ if (ts.isIdentifier(element))
38
+ return [{ variableName: element.text, arrayIndex }];
39
+ return unsupported(bindingAssignmentEntries(element));
40
+ }
41
+
42
+ function assignmentTarget(
43
+ expression: ts.Expression,
44
+ ): { expression: ts.Expression; unsupported: boolean } {
45
+ if (ts.isParenthesizedExpression(expression))
46
+ return assignmentTarget(expression.expression);
47
+ if (ts.isAsExpression(expression)
48
+ || ts.isSatisfiesExpression(expression)
49
+ || ts.isTypeAssertionExpression(expression)
50
+ || ts.isNonNullExpression(expression)) {
51
+ const target = assignmentTarget(expression.expression);
52
+ return { expression: target.expression, unsupported: true };
53
+ }
54
+ if (ts.isBinaryExpression(expression)
55
+ && expression.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
56
+ const target = assignmentTarget(expression.left);
57
+ return { expression: target.expression, unsupported: true };
58
+ }
59
+ return { expression, unsupported: false };
60
+ }
61
+
62
+ export function bindingAssignmentEntries(
63
+ expression: ts.Expression,
64
+ ): BindingAssignmentEntry[] {
65
+ const target = assignmentTarget(expression);
66
+ let entries: BindingAssignmentEntry[] = [];
67
+ if (ts.isIdentifier(target.expression))
68
+ entries = [{ variableName: target.expression.text }];
69
+ if (ts.isObjectLiteralExpression(target.expression))
70
+ entries = target.expression.properties.flatMap(objectEntries);
71
+ if (ts.isArrayLiteralExpression(target.expression))
72
+ entries = target.expression.elements.flatMap(arrayEntries);
73
+ if (ts.isSpreadElement(target.expression))
74
+ entries = unsupported(bindingAssignmentEntries(target.expression.expression));
75
+ return target.unsupported ? unsupported(entries) : entries;
76
+ }
@@ -0,0 +1,217 @@
1
+ import ts from 'typescript';
2
+ import {
3
+ identifierMatchesDeclaration,
4
+ } from './002-symbol-import-bindings.js';
5
+ import {
6
+ isUnshadowedCommonJsExportExpression,
7
+ } from './018-package-commonjs-syntax.js';
8
+
9
+ function transparentUse(node: ts.Node): ts.Node {
10
+ const parent = node.parent;
11
+ if (parent && (ts.isParenthesizedExpression(parent)
12
+ || ts.isAsExpression(parent)
13
+ || ts.isSatisfiesExpression(parent)
14
+ || ts.isTypeAssertionExpression(parent)
15
+ || ts.isNonNullExpression(parent)))
16
+ return transparentUse(parent);
17
+ return node;
18
+ }
19
+
20
+ function assignmentOperator(kind: ts.SyntaxKind): boolean {
21
+ return kind >= ts.SyntaxKind.FirstAssignment
22
+ && kind <= ts.SyntaxKind.LastAssignment;
23
+ }
24
+
25
+ function directWriteTarget(node: ts.Node): boolean {
26
+ const use = transparentUse(node);
27
+ const parent = use.parent;
28
+ if (!parent) return false;
29
+ if (ts.isBinaryExpression(parent))
30
+ return parent.left === use && assignmentOperator(parent.operatorToken.kind);
31
+ if (ts.isDeleteExpression(parent))
32
+ return parent.expression === use;
33
+ if (ts.isPrefixUnaryExpression(parent)
34
+ || ts.isPostfixUnaryExpression(parent))
35
+ return parent.operand === use;
36
+ return (ts.isForInStatement(parent) || ts.isForOfStatement(parent))
37
+ && parent.initializer === use;
38
+ }
39
+
40
+ type MemberContainer = ts.ObjectLiteralExpression | ts.ClassLikeDeclaration;
41
+ type StableMember = ts.ObjectLiteralElementLike | ts.ClassElement;
42
+
43
+ function memberContainer(
44
+ declaration: ts.Identifier,
45
+ ): MemberContainer | undefined {
46
+ const parent = declaration.parent;
47
+ if (ts.isClassDeclaration(parent)) return parent;
48
+ if (!ts.isVariableDeclaration(parent) || parent.name !== declaration
49
+ || !parent.initializer) return undefined;
50
+ return ts.isObjectLiteralExpression(parent.initializer)
51
+ || ts.isClassExpression(parent.initializer)
52
+ ? parent.initializer
53
+ : undefined;
54
+ }
55
+
56
+ function declaredMemberName(node: ts.Node): string | undefined {
57
+ if (ts.isIdentifier(node) || ts.isPrivateIdentifier(node))
58
+ return node.text;
59
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)
60
+ || ts.isNumericLiteral(node)) return node.text;
61
+ return undefined;
62
+ }
63
+
64
+ function accessedMemberName(node: ts.Node): string | undefined {
65
+ if (ts.isPropertyAccessExpression(node)) return node.name.text;
66
+ if (!ts.isElementAccessExpression(node)) return undefined;
67
+ const argument = node.argumentExpression;
68
+ return argument && (ts.isStringLiteral(argument)
69
+ || ts.isNoSubstitutionTemplateLiteral(argument))
70
+ ? argument.text
71
+ : undefined;
72
+ }
73
+
74
+ function memberName(node: ts.Node): string | undefined {
75
+ return declaredMemberName(node) ?? accessedMemberName(node);
76
+ }
77
+
78
+ function matchingMember(
79
+ value: MemberContainer,
80
+ name: string,
81
+ ): StableMember | undefined {
82
+ const members = ts.isObjectLiteralExpression(value)
83
+ ? value.properties
84
+ : value.members.filter((member) =>
85
+ ts.canHaveModifiers(member)
86
+ && ts.getModifiers(member)?.some((modifier) =>
87
+ modifier.kind === ts.SyntaxKind.StaticKeyword));
88
+ const matches = members.filter((member) =>
89
+ 'name' in member && member.name && memberName(member.name) === name);
90
+ if (matches.length === 1) return matches[0];
91
+ const implemented = matches.filter((member) => callableMember(member)?.body);
92
+ return implemented.length === 1 ? implemented[0] : undefined;
93
+ }
94
+
95
+ function callableMember(
96
+ member: StableMember,
97
+ ): ts.FunctionLikeDeclaration | undefined {
98
+ if (ts.isMethodDeclaration(member)) return member;
99
+ if (ts.isPropertyAssignment(member)
100
+ && (ts.isArrowFunction(member.initializer)
101
+ || ts.isFunctionExpression(member.initializer)))
102
+ return member.initializer;
103
+ if (ts.isPropertyDeclaration(member) && member.initializer
104
+ && (ts.isArrowFunction(member.initializer)
105
+ || ts.isFunctionExpression(member.initializer)))
106
+ return member.initializer;
107
+ return undefined;
108
+ }
109
+
110
+ function containsThis(node: ts.Node): boolean {
111
+ let found = false;
112
+ const visit = (child: ts.Node): void => {
113
+ if (found) return;
114
+ if (child.kind === ts.SyntaxKind.ThisKeyword) {
115
+ found = true;
116
+ return;
117
+ }
118
+ ts.forEachChild(child, visit);
119
+ };
120
+ visit(node);
121
+ return found;
122
+ }
123
+
124
+ function invokedMember(node: ts.Node): boolean {
125
+ const use = transparentUse(node);
126
+ const parent = use.parent;
127
+ return Boolean(parent && ts.isCallExpression(parent)
128
+ && parent.expression === use);
129
+ }
130
+
131
+ type MemberAccess = ts.PropertyAccessExpression | ts.ElementAccessExpression;
132
+
133
+ function receiverAccess(
134
+ node: ts.Node,
135
+ ): MemberAccess | undefined {
136
+ const parent = node.parent;
137
+ if (!parent || (!ts.isPropertyAccessExpression(parent)
138
+ && !ts.isElementAccessExpression(parent))
139
+ || parent.expression !== node || directWriteTarget(parent)) return undefined;
140
+ return parent;
141
+ }
142
+
143
+ function stableMemberUse(
144
+ access: MemberAccess,
145
+ member: StableMember,
146
+ ): boolean {
147
+ if (!invokedMember(access)) return !ts.isGetAccessor(member)
148
+ && !ts.isSetAccessor(member);
149
+ const callable = callableMember(member);
150
+ return Boolean(callable && !containsThis(callable));
151
+ }
152
+
153
+ function memberReceiverUse(
154
+ node: ts.Node,
155
+ declaration: ts.Identifier,
156
+ ): boolean {
157
+ const access = receiverAccess(node);
158
+ if (!access) return false;
159
+ const name = memberName(access);
160
+ const value = memberContainer(declaration);
161
+ const member = name && value ? matchingMember(value, name) : undefined;
162
+ return member ? stableMemberUse(access, member) : false;
163
+ }
164
+
165
+ function commonJsExportUse(node: ts.Node): boolean {
166
+ const parent = node.parent;
167
+ return Boolean(parent && ts.isBinaryExpression(parent)
168
+ && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken
169
+ && parent.right === node
170
+ && isUnshadowedCommonJsExportExpression(parent.left)
171
+ && ts.isExpressionStatement(parent.parent)
172
+ && ts.isSourceFile(parent.parent.parent));
173
+ }
174
+
175
+ function esmExportUse(node: ts.Node): boolean {
176
+ const parent = node.parent;
177
+ return Boolean(parent && (ts.isExportSpecifier(parent)
178
+ || (ts.isExportAssignment(parent) && parent.expression === node)));
179
+ }
180
+
181
+ function inertUnaryUse(node: ts.Node): boolean {
182
+ const parent = node.parent;
183
+ return Boolean(parent && ((ts.isVoidExpression(parent)
184
+ || ts.isTypeOfExpression(parent)) && parent.expression === node));
185
+ }
186
+
187
+ function safeReferenceUse(
188
+ node: ts.Identifier,
189
+ declaration: ts.Identifier,
190
+ ): boolean {
191
+ const use = transparentUse(node);
192
+ return memberReceiverUse(use, declaration)
193
+ || commonJsExportUse(use)
194
+ || esmExportUse(use)
195
+ || inertUnaryUse(use);
196
+ }
197
+
198
+ export function stableLocalValueReference(
199
+ source: ts.SourceFile,
200
+ declaration: ts.Identifier,
201
+ ): boolean {
202
+ const start = declaration.getStart(source);
203
+ const end = declaration.getEnd();
204
+ let stable = true;
205
+ const visit = (node: ts.Node): void => {
206
+ if (!stable) return;
207
+ if (ts.isIdentifier(node) && node !== declaration
208
+ && identifierMatchesDeclaration(node, start, end)
209
+ && !safeReferenceUse(node, declaration)) {
210
+ stable = false;
211
+ return;
212
+ }
213
+ ts.forEachChild(node, visit);
214
+ };
215
+ visit(source);
216
+ return stable;
217
+ }
@@ -0,0 +1,152 @@
1
+ import ts from 'typescript';
2
+ import type { LexicalScopeFact } from '../types.js';
3
+ import {
4
+ declarationAt,
5
+ enclosingCaseClause,
6
+ executionScope,
7
+ lexicalScopeChain,
8
+ sameScope,
9
+ type BindingLexicalIndex,
10
+ type BindingLexicalSite,
11
+ type BindingSiteCandidate,
12
+ type VisibleBinding,
13
+ } from './011-binding-lexical-scope.js';
14
+
15
+ function reachingSites(
16
+ index: BindingLexicalIndex,
17
+ variableName: string,
18
+ useStart: number,
19
+ useChain: readonly LexicalScopeFact[],
20
+ declaration: BindingLexicalSite,
21
+ ): BindingLexicalSite[] {
22
+ const useExecution = executionScope(useChain);
23
+ return index.sites.filter((site) =>
24
+ site.variableName === variableName
25
+ && site.declarationKey === declaration.declarationKey
26
+ && site.startOffset < useStart
27
+ && (site.flow === 'declaration'
28
+ || sameScope(site.executionScope, useExecution)))
29
+ .sort((left, right) =>
30
+ right.startOffset - left.startOffset
31
+ || right.endOffset - left.endOffset);
32
+ }
33
+
34
+ function enclosingLoops(node: ts.Node): ts.IterationStatement[] {
35
+ const loops: ts.IterationStatement[] = [];
36
+ let current: ts.Node | undefined = node.parent;
37
+ while (current && !ts.isSourceFile(current)
38
+ && !ts.isFunctionLike(current)) {
39
+ if (ts.isIterationStatement(current, false)) loops.push(current);
40
+ current = current.parent;
41
+ }
42
+ return loops;
43
+ }
44
+
45
+ function unsupportedAssignmentFlow(
46
+ index: BindingLexicalIndex,
47
+ variableName: string,
48
+ useNode: ts.Node,
49
+ useStart: number,
50
+ useChain: readonly LexicalScopeFact[],
51
+ declaration: BindingLexicalSite,
52
+ ): boolean {
53
+ const useExecution = executionScope(useChain);
54
+ const loops = enclosingLoops(useNode);
55
+ return index.sites.some((site) => {
56
+ if (site.flow !== 'assignment'
57
+ || site.variableName !== variableName
58
+ || site.declarationKey !== declaration.declarationKey) return false;
59
+ if (!sameScope(site.executionScope, useExecution)) return true;
60
+ return site.startOffset > useStart && loops.some((loop) =>
61
+ site.node.pos >= loop.pos && site.node.end <= loop.end);
62
+ });
63
+ }
64
+
65
+ function candidatesAtSite<T>(
66
+ candidates: readonly BindingSiteCandidate<T>[],
67
+ site: BindingLexicalSite,
68
+ ): BindingSiteCandidate<T>[] {
69
+ return candidates.filter((candidate) =>
70
+ candidate.variableName === site.variableName
71
+ && candidate.bindingSiteStartOffset === site.startOffset
72
+ && candidate.bindingSiteEndOffset === site.endOffset);
73
+ }
74
+
75
+ function compatibleCaseClause(
76
+ site: BindingLexicalSite,
77
+ useNode: ts.Node,
78
+ source: ts.SourceFile,
79
+ ): boolean {
80
+ if (site.caseClauseStartOffset === undefined
81
+ || site.caseClauseEndOffset === undefined) return true;
82
+ const clause = enclosingCaseClause(useNode);
83
+ return clause?.getStart(source) === site.caseClauseStartOffset
84
+ && clause.getEnd() === site.caseClauseEndOffset;
85
+ }
86
+
87
+ type VisibleBindingReason = NonNullable<VisibleBinding<unknown>['reason']>;
88
+
89
+ function declarationFailure(
90
+ declaration: BindingLexicalSite,
91
+ after: boolean,
92
+ useNode: ts.Node,
93
+ source: ts.SourceFile,
94
+ ): VisibleBindingReason | undefined {
95
+ if (declaration.flow === 'shadow') return 'binding_flow_unsupported';
96
+ if (declaration.declarationKind === 'var') return 'unsupported_var_binding';
97
+ if (!compatibleCaseClause(declaration, useNode, source))
98
+ return 'binding_flow_unsupported';
99
+ return after ? 'binding_declared_after_call' : undefined;
100
+ }
101
+
102
+ function matchedBinding<T>(
103
+ candidates: readonly BindingSiteCandidate<T>[],
104
+ site: BindingLexicalSite,
105
+ ): VisibleBinding<T> | undefined {
106
+ const matches = candidatesAtSite(candidates, site);
107
+ if (matches.length === 0) return {
108
+ status: 'unresolved',
109
+ reason: site.flow === 'assignment'
110
+ ? 'unsupported_reaching_assignment'
111
+ : 'binding_not_found',
112
+ };
113
+ if (matches.length > 1) return { status: 'ambiguous' };
114
+ return matches[0] ? { status: 'resolved', candidate: matches[0] } : undefined;
115
+ }
116
+
117
+ export function selectVisibleBinding<T>(
118
+ index: BindingLexicalIndex,
119
+ candidates: readonly BindingSiteCandidate<T>[],
120
+ variableName: string,
121
+ useNode: ts.Node,
122
+ ): VisibleBinding<T> {
123
+ const useStart = useNode.getStart(index.source);
124
+ const useChain = lexicalScopeChain(useNode, index.source);
125
+ const declaration = declarationAt(index.sites, variableName, useStart, useChain);
126
+ if (declaration.ambiguous) return { status: 'ambiguous' };
127
+ if (!declaration.site) return { status: 'unresolved', reason: 'binding_not_found' };
128
+ const failure = declarationFailure(
129
+ declaration.site, declaration.after, useNode, index.source,
130
+ );
131
+ if (failure) return { status: 'unresolved', reason: failure };
132
+ if (unsupportedAssignmentFlow(
133
+ index, variableName, useNode, useStart, useChain, declaration.site,
134
+ )) return {
135
+ status: 'unresolved',
136
+ reason: 'unsupported_reaching_assignment',
137
+ };
138
+ const site = reachingSites(
139
+ index, variableName, useStart, useChain, declaration.site,
140
+ )[0];
141
+ if (!site) return { status: 'unresolved', reason: 'binding_not_found' };
142
+ const matched = matchedBinding(candidates, site);
143
+ if (!matched || matched.status !== 'resolved') return matched ?? {
144
+ status: 'unresolved', reason: 'binding_not_found',
145
+ };
146
+ return {
147
+ ...matched,
148
+ site,
149
+ declarationSite: declaration.site,
150
+ scopeIndex: declaration.scopeIndex,
151
+ };
152
+ }
@@ -3,6 +3,7 @@ import {
3
3
  classifyODataPathIntent,
4
4
  normalizeODataOperationInvocationPath,
5
5
  } from '../linker/odata-path-normalizer.js';
6
+ import { analyzeODataPathStructure } from '../linker/005-odata-path-structure.js';
6
7
 
7
8
  export type OperationPathStatus = 'static' | 'ambiguous' | 'dynamic' | 'unknown';
8
9
 
@@ -284,7 +285,11 @@ function normalizedCandidate(value: string, method: string): string[] {
284
285
  if (invocation?.wasInvocation) return [invocation.normalizedOperationPath];
285
286
  const intent = classifyODataPathIntent(value, method);
286
287
  if (intent.kind.startsWith('entity_')) return [];
287
- if (!value.startsWith('/') || value.slice(1).includes('/') || value.includes('?')) return [];
288
+ const structure = analyzeODataPathStructure(value);
289
+ if (structure.status !== 'valid' || !value.startsWith('/')
290
+ || structure.segments.length !== 1
291
+ || structure.queryIndex !== undefined)
292
+ return [];
288
293
  return [value];
289
294
  }
290
295
 
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import ts from 'typescript';
4
4
  import { externalHttpTarget } from '../linker/external-http-target.js';
5
- import type { OutboundCallFact } from '../types.js';
5
+ import type { OutboundCallFact, ServiceBindingFact } from '../types.js';
6
6
  import { normalizePath, stripQuotes } from '../utils/path-utils.js';
7
7
  import { summarizeExpression } from '../utils/redaction.js';
8
8
  import { classifyODataPathIntent } from '../linker/odata-path-normalizer.js';
@@ -461,16 +461,23 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
461
461
  visit(source);
462
462
  return calls;
463
463
  }
464
- export function containsSupportedOutboundCall(node: ts.Node): boolean {
464
+ export function containsSupportedOutboundCall(
465
+ node: ts.Node,
466
+ classified?: readonly ClassifiedOutboundCall[],
467
+ ): boolean {
465
468
  const source = node.getSourceFile();
466
469
  const start = node.getFullStart();
467
470
  const end = node.getEnd();
468
- return classifyOutboundCallsInSource(source, source.fileName).some((call) => call.node.getStart(source) >= start && call.node.getEnd() <= end);
471
+ const calls = classified ?? classifyOutboundCallsInSource(source, source.fileName);
472
+ return calls.some((call) =>
473
+ call.node.getStart(source) >= start && call.node.getEnd() <= end);
469
474
  }
470
475
  export async function parseOutboundCalls(
471
476
  repoPath: string,
472
477
  filePath: string,
473
478
  context?: RepositorySourceContext,
479
+ classified?: readonly ClassifiedOutboundCall[],
480
+ preparedBindings?: readonly ServiceBindingFact[],
474
481
  ): Promise<OutboundCallFact[]> {
475
482
  const snapshot = context?.get(filePath);
476
483
  const text = snapshot?.text
@@ -479,13 +486,19 @@ export async function parseOutboundCalls(
479
486
  filePath, text, ts.ScriptTarget.Latest, true,
480
487
  filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS,
481
488
  );
482
- const bindingNames = new Set((await parseServiceBindings(
489
+ const bindings = preparedBindings ?? await parseServiceBindings(
483
490
  repoPath, filePath, context,
484
- )).map((binding) => binding.variableName));
491
+ );
492
+ const bindingNames = new Set(bindings.map((binding) => binding.variableName));
485
493
  const importedWrappers = await parseImportedWrapperCalls(
486
494
  repoPath, filePath, source, bindingNames, context,
487
495
  );
488
- return [...classifyOutboundCallsInSource(source, filePath).map((call) => call.fact), ...importedWrappers, ...parseLocalServiceCalls(text, filePath, source)];
496
+ const nativeCalls = classified ?? classifyOutboundCallsInSource(source, filePath);
497
+ return [
498
+ ...nativeCalls.map((call) => call.fact),
499
+ ...importedWrappers,
500
+ ...parseLocalServiceCalls(text, filePath, source),
501
+ ];
489
502
  }
490
503
  function parseLocalServiceCalls(
491
504
  text: string,
@@ -7,11 +7,49 @@ interface ParsePackageJsonOptions {
7
7
  }
8
8
  export interface PackageJsonSnapshot {
9
9
  facts: PackageFacts;
10
+ manifest: PackageEntrypointManifest;
10
11
  rawText: string;
11
12
  }
13
+ export interface PackageEntrypointManifest {
14
+ mainPresent: boolean;
15
+ main: string | null;
16
+ modulePresent: boolean;
17
+ module: string | null;
18
+ exportsPresent: boolean;
19
+ exportsValue: unknown;
20
+ }
21
+ function emptyManifest(): PackageEntrypointManifest {
22
+ return {
23
+ mainPresent: false,
24
+ main: null,
25
+ modulePresent: false,
26
+ module: null,
27
+ exportsPresent: false,
28
+ exportsValue: null,
29
+ };
30
+ }
31
+ function entrypointManifest(
32
+ json: Record<string, unknown>,
33
+ ): PackageEntrypointManifest {
34
+ const mainPresent = Object.hasOwn(json, 'main');
35
+ const modulePresent = Object.hasOwn(json, 'module');
36
+ const exportsPresent = Object.hasOwn(json, 'exports');
37
+ return {
38
+ mainPresent,
39
+ main: typeof json.main === 'string' ? json.main : null,
40
+ modulePresent,
41
+ module: typeof json.module === 'string' ? json.module : null,
42
+ exportsPresent,
43
+ exportsValue: exportsPresent ? json.exports : null,
44
+ };
45
+ }
12
46
  function emptyPackageFacts(): PackageFacts {
13
47
  return { dependencies: {}, cdsRequires: [], scripts: {} };
14
48
  }
49
+ function isMissingFileError(error: unknown): boolean {
50
+ return typeof error === 'object' && error !== null
51
+ && 'code' in error && error.code === 'ENOENT';
52
+ }
15
53
  function recordOfString(value: unknown): Record<string, string> {
16
54
  if (!value || typeof value !== 'object') return {};
17
55
  return Object.fromEntries(
@@ -66,6 +104,7 @@ export async function loadPackageJsonSnapshot(
66
104
  const json = JSON.parse(raw) as Record<string, unknown>;
67
105
  return {
68
106
  rawText: raw,
107
+ manifest: entrypointManifest(json),
69
108
  facts: {
70
109
  packageName: typeof json.name === 'string' ? json.name : undefined,
71
110
  packageVersion:
@@ -79,10 +118,13 @@ export async function loadPackageJsonSnapshot(
79
118
  },
80
119
  };
81
120
  } catch (error) {
82
- const missing = typeof error === 'object' && error !== null
83
- && 'code' in error && error.code === 'ENOENT';
121
+ const missing = isMissingFileError(error);
84
122
  if (!options.strict || (options.allowMissing && missing))
85
- return { facts: emptyPackageFacts(), rawText: '' };
123
+ return {
124
+ facts: emptyPackageFacts(),
125
+ manifest: emptyManifest(),
126
+ rawText: '',
127
+ };
86
128
  throw error;
87
129
  }
88
130
  }