@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.
- package/CHANGELOG.md +10 -0
- package/README.md +20 -8
- package/TECHNICAL-NOTE.md +27 -0
- package/dist/{chunk-ZQABU7MR.js → chunk-AEM4JY22.js} +12676 -6714
- package/dist/chunk-AEM4JY22.js.map +1 -0
- package/dist/cli.js +2325 -413
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +61 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/003-doctor-package-resolution.ts +68 -0
- package/src/cli/doctor.ts +25 -14
- package/src/db/000-call-fact-repository.ts +475 -342
- package/src/db/001-fact-lifecycle.ts +60 -30
- package/src/db/002-fact-json-inventory.ts +169 -0
- package/src/db/003-current-fact-semantics.ts +698 -0
- package/src/db/004-package-target-invalidation.ts +173 -0
- package/src/db/005-schema-structure.ts +201 -0
- package/src/db/006-relative-symbol-resolution.ts +443 -0
- package/src/db/007-package-fact-semantics.ts +573 -0
- package/src/db/008-relative-fact-semantics.ts +207 -0
- package/src/db/009-binding-fact-semantics.ts +347 -0
- package/src/db/010-package-symbol-surface-semantics.ts +320 -0
- package/src/db/011-symbol-call-semantics.ts +144 -0
- package/src/db/012-binding-reference-proof.ts +264 -0
- package/src/db/migrations.ts +16 -3
- package/src/db/repositories.ts +113 -6
- package/src/db/schema.ts +4 -2
- package/src/index.ts +12 -0
- package/src/indexer/repository-indexer.ts +72 -11
- package/src/indexer/workspace-indexer.ts +123 -32
- package/src/linker/003-package-import-symbol-resolver.ts +363 -131
- package/src/linker/004-event-subscription-handler-linker.ts +2 -0
- package/src/linker/005-odata-path-structure.ts +371 -0
- package/src/linker/cross-repo-linker.ts +2 -1
- package/src/linker/odata-path-normalizer.ts +273 -180
- package/src/linker/service-resolver.ts +197 -77
- package/src/parsers/002-symbol-import-bindings.ts +516 -0
- package/src/parsers/003-package-public-surface.ts +661 -0
- package/src/parsers/004-fact-identity.ts +108 -0
- package/src/parsers/005-event-subscription-facts.ts +281 -0
- package/src/parsers/006-binding-identity.ts +343 -0
- package/src/parsers/007-source-fact-reconciliation.ts +105 -0
- package/src/parsers/008-package-surface-publication.ts +82 -0
- package/src/parsers/009-symbol-call-facts.ts +528 -0
- package/src/parsers/010-package-public-surface-analysis.ts +352 -0
- package/src/parsers/011-binding-lexical-scope.ts +583 -0
- package/src/parsers/012-package-fact-contract.ts +306 -0
- package/src/parsers/013-executable-body-eligibility.ts +35 -0
- package/src/parsers/014-service-binding-helper-flow.ts +240 -0
- package/src/parsers/015-service-binding-collector.ts +693 -0
- package/src/parsers/016-local-symbol-reference.ts +261 -0
- package/src/parsers/017-symbol-derived-contexts.ts +268 -0
- package/src/parsers/018-package-commonjs-syntax.ts +142 -0
- package/src/parsers/019-binding-assignment-targets.ts +76 -0
- package/src/parsers/020-stable-local-value.ts +217 -0
- package/src/parsers/021-binding-visibility.ts +152 -0
- package/src/parsers/operation-path-analysis.ts +6 -1
- package/src/parsers/outbound-call-parser.ts +19 -6
- package/src/parsers/package-json-parser.ts +45 -3
- package/src/parsers/service-binding-parser-helpers.ts +86 -15
- package/src/parsers/service-binding-parser.ts +147 -597
- package/src/parsers/symbol-parser.ts +482 -353
- package/src/trace/002-trace-diagnostics.ts +36 -1
- package/src/trace/016-compact-projector.ts +16 -17
- package/src/trace/020-compact-field-projection.ts +48 -37
- package/src/trace/021-compact-decision-normalization.ts +105 -0
- package/src/trace/022-trace-fact-preflight.ts +21 -0
- package/src/trace/023-nested-event-scopes.ts +23 -0
- package/src/trace/024-compact-observation-decision.ts +76 -0
- package/src/trace/025-trace-implementation-scope.ts +123 -0
- package/src/trace/026-trace-start-scope.ts +335 -0
- package/src/trace/027-trace-scope-execution.ts +566 -0
- package/src/trace/028-trace-operation-execution.ts +336 -0
- package/src/trace/029-trace-start-implementation.ts +172 -0
- package/src/trace/trace-engine.ts +122 -624
- package/src/types.ts +56 -0
- package/src/utils/001-placeholders.ts +188 -10
- package/src/version.ts +1 -1
- 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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
489
|
+
const bindings = preparedBindings ?? await parseServiceBindings(
|
|
483
490
|
repoPath, filePath, context,
|
|
484
|
-
)
|
|
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
|
-
|
|
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 =
|
|
83
|
-
&& 'code' in error && error.code === 'ENOENT';
|
|
121
|
+
const missing = isMissingFileError(error);
|
|
84
122
|
if (!options.strict || (options.allowMissing && missing))
|
|
85
|
-
return {
|
|
123
|
+
return {
|
|
124
|
+
facts: emptyPackageFacts(),
|
|
125
|
+
manifest: emptyManifest(),
|
|
126
|
+
rawText: '',
|
|
127
|
+
};
|
|
86
128
|
throw error;
|
|
87
129
|
}
|
|
88
130
|
}
|