@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.
- package/CHANGELOG.md +20 -0
- package/README.md +27 -9
- package/TECHNICAL-NOTE.md +36 -0
- package/dist/chunk-3N3B5KHV.js +19596 -0
- package/dist/chunk-3N3B5KHV.js.map +1 -0
- package/dist/cli.js +2645 -521
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +67 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/001-index-summary.ts +22 -0
- package/src/cli/003-doctor-package-resolution.ts +68 -0
- package/src/cli/doctor.ts +25 -14
- package/src/cli.ts +151 -87
- package/src/db/000-call-fact-repository.ts +499 -340
- 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 +699 -0
- package/src/db/004-package-target-invalidation.ts +183 -0
- package/src/db/005-schema-structure.ts +201 -0
- package/src/db/006-relative-symbol-resolution.ts +464 -0
- package/src/db/007-package-fact-semantics.ts +573 -0
- package/src/db/008-relative-fact-semantics.ts +210 -0
- package/src/db/009-binding-fact-semantics.ts +352 -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 +268 -0
- package/src/db/013-index-publication-failure.ts +91 -0
- package/src/db/014-binding-helper-provenance.ts +17 -0
- package/src/db/migrations.ts +16 -3
- package/src/db/repositories.ts +130 -6
- package/src/db/schema.ts +4 -2
- package/src/index.ts +12 -0
- package/src/indexer/cds-extension-resolver.ts +27 -3
- package/src/indexer/repository-indexer.ts +135 -13
- package/src/indexer/workspace-indexer.ts +237 -34
- package/src/linker/003-package-import-symbol-resolver.ts +363 -131
- package/src/linker/004-event-subscription-handler-linker.ts +34 -11
- package/src/linker/005-odata-path-structure.ts +371 -0
- package/src/linker/006-event-template-link.ts +72 -0
- package/src/linker/007-call-edge-insertion.ts +568 -0
- package/src/linker/cross-repo-linker.ts +4 -166
- package/src/linker/odata-path-normalizer.ts +273 -180
- package/src/linker/service-resolver.ts +197 -77
- package/src/parsers/000-direct-query-execution.ts +11 -0
- 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 +348 -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 +306 -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 +168 -0
- package/src/parsers/022-outbound-expression-analysis.ts +700 -0
- package/src/parsers/023-outbound-call-classifier.ts +692 -0
- package/src/parsers/operation-path-analysis.ts +6 -1
- package/src/parsers/outbound-call-parser.ts +162 -512
- 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 +513 -352
- package/src/trace/002-trace-diagnostics.ts +36 -1
- package/src/trace/007-implementation-start-diagnostic.ts +1 -0
- package/src/trace/011-event-subscriber-traversal.ts +100 -8
- package/src/trace/013-trace-root-scopes.ts +2 -3
- package/src/trace/014-compact-contract.ts +6 -0
- package/src/trace/015-trace-edge-recorder.ts +61 -4
- package/src/trace/016-compact-projector.ts +15 -17
- package/src/trace/019-trace-edge-semantics.ts +6 -10
- package/src/trace/020-compact-field-projection.ts +122 -38
- package/src/trace/021-compact-decision-normalization.ts +171 -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 +81 -0
- package/src/trace/025-trace-implementation-scope.ts +123 -0
- package/src/trace/026-trace-start-scope.ts +336 -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/030-event-runtime-resolution.ts +151 -0
- package/src/trace/031-local-call-expansion.ts +37 -0
- package/src/trace/implementation-hints.ts +9 -6
- package/src/trace/selectors.ts +1 -0
- package/src/trace/trace-engine.ts +122 -624
- package/src/types.ts +57 -0
- package/src/utils/001-placeholders.ts +188 -10
- package/src/version.ts +1 -1
- package/dist/chunk-ZQABU7MR.js +0 -12151
- package/dist/chunk-ZQABU7MR.js.map +0 -1
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import {
|
|
3
|
+
chainIncludesForUpdate,
|
|
4
|
+
queryBuilderRoot,
|
|
5
|
+
type DirectQueryBuilderStatement,
|
|
6
|
+
} from './000-direct-query-execution.js';
|
|
7
|
+
import {
|
|
8
|
+
expressionName,
|
|
9
|
+
maxAliasDepth,
|
|
10
|
+
resolveBinding,
|
|
11
|
+
} from './001-query-entity-resolution.js';
|
|
12
|
+
import type { OperationPathAnalysis } from './operation-path-analysis.js';
|
|
13
|
+
import { stripQuotes } from '../utils/path-utils.js';
|
|
14
|
+
export function lineOf(text: string, index: number): number {
|
|
15
|
+
return text.slice(0, index).split('\n').length;
|
|
16
|
+
}
|
|
17
|
+
export function queryBuilderEvidence(
|
|
18
|
+
source: ts.SourceFile,
|
|
19
|
+
statement: DirectQueryBuilderStatement,
|
|
20
|
+
): Record<string, unknown> {
|
|
21
|
+
const hasForUpdate = chainIncludesForUpdate(statement.logicalCall);
|
|
22
|
+
return {
|
|
23
|
+
classifier: 'cap_query_builder_direct',
|
|
24
|
+
queryDispatch: 'direct_query_builder',
|
|
25
|
+
queryRoot: expressionName(statement.root.expression),
|
|
26
|
+
queryRootStartOffset: statement.root.getStart(source),
|
|
27
|
+
queryRootEndOffset: statement.root.getEnd(),
|
|
28
|
+
queryStatementStartOffset: statement.statement.getStart(source),
|
|
29
|
+
queryStatementEndOffset: statement.statement.getEnd(),
|
|
30
|
+
queryExecutionContext: statement.executionContext,
|
|
31
|
+
...(hasForUpdate ? { hasForUpdate: true } : {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function queryRunEvidence(
|
|
35
|
+
source: ts.SourceFile,
|
|
36
|
+
argument: ts.Expression | undefined,
|
|
37
|
+
): Record<string, unknown> {
|
|
38
|
+
const root = argument ? queryBuilderRoot(argument) : undefined;
|
|
39
|
+
const hasForUpdate = argument
|
|
40
|
+
? chainIncludesForUpdate(argument) : false;
|
|
41
|
+
return {
|
|
42
|
+
classifier: 'cap_query_run_wrapper',
|
|
43
|
+
queryDispatch: 'cds_run_wrapper',
|
|
44
|
+
...(root ? {
|
|
45
|
+
queryRoot: expressionName(root.expression),
|
|
46
|
+
queryRootStartOffset: root.getStart(source),
|
|
47
|
+
queryRootEndOffset: root.getEnd(),
|
|
48
|
+
} : {}),
|
|
49
|
+
...(hasForUpdate ? { hasForUpdate: true } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function queryWarning(expression: string): string {
|
|
53
|
+
if (/^\s*[`'"]/.test(expression)) return 'raw_sql_or_cql_expression';
|
|
54
|
+
if (/^\s*\w+\s*$/.test(expression))
|
|
55
|
+
return 'query_variable_without_static_initializer';
|
|
56
|
+
return 'dynamic_entity_expression';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function parserEvidence(
|
|
60
|
+
source: ts.SourceFile,
|
|
61
|
+
node: ts.CallExpression,
|
|
62
|
+
extra?: Record<string, unknown>,
|
|
63
|
+
): Record<string, unknown> {
|
|
64
|
+
return {
|
|
65
|
+
parser: 'typescript_ast',
|
|
66
|
+
startOffset: node.getStart(source),
|
|
67
|
+
endOffset: node.getEnd(),
|
|
68
|
+
...extra,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isStringLike(
|
|
73
|
+
expression: ts.Expression | undefined,
|
|
74
|
+
): expression is ts.StringLiteral | ts.NoSubstitutionTemplateLiteral {
|
|
75
|
+
return Boolean(expression && (
|
|
76
|
+
ts.isStringLiteral(expression)
|
|
77
|
+
|| ts.isNoSubstitutionTemplateLiteral(expression)
|
|
78
|
+
));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function literalText(
|
|
82
|
+
expression: ts.Expression | undefined,
|
|
83
|
+
): string | undefined {
|
|
84
|
+
return isStringLike(expression) ? expression.text : undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const CDS_LIFECYCLE_EVENTS = new Set([
|
|
88
|
+
'bootstrap', 'loaded', 'connect', 'serving', 'served', 'listening',
|
|
89
|
+
'shutdown',
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
export function objectPropertyIsShorthand(
|
|
93
|
+
object: ts.ObjectLiteralExpression,
|
|
94
|
+
key: string,
|
|
95
|
+
): boolean {
|
|
96
|
+
return object.properties.some((property) =>
|
|
97
|
+
ts.isShorthandPropertyAssignment(property)
|
|
98
|
+
&& property.name.text === key);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function nameOfProperty(name: ts.PropertyName): string | undefined {
|
|
102
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name)
|
|
103
|
+
|| ts.isNumericLiteral(name)) return name.text;
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type ExpressionStatus = 'static' | 'dynamic' | 'ambiguous' | 'unknown';
|
|
108
|
+
type ExpressionSourceKind =
|
|
109
|
+
| 'string_literal'
|
|
110
|
+
| 'no_substitution_template'
|
|
111
|
+
| 'template_with_substitutions'
|
|
112
|
+
| 'const_alias'
|
|
113
|
+
| 'conditional_candidates'
|
|
114
|
+
| 'dynamic_expression';
|
|
115
|
+
|
|
116
|
+
export interface ExpressionResolution {
|
|
117
|
+
status: ExpressionStatus;
|
|
118
|
+
sourceKind: ExpressionSourceKind;
|
|
119
|
+
value?: string;
|
|
120
|
+
rawExpression?: string;
|
|
121
|
+
placeholderKeys: string[];
|
|
122
|
+
evidence: string[];
|
|
123
|
+
constName?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function safeRaw(expression: ts.Expression): string | undefined {
|
|
127
|
+
if (ts.isStringLiteral(expression)
|
|
128
|
+
|| ts.isNoSubstitutionTemplateLiteral(expression)
|
|
129
|
+
|| ts.isIdentifier(expression)
|
|
130
|
+
|| ts.isTemplateExpression(expression))
|
|
131
|
+
return expression.getText(expression.getSourceFile());
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function staticResolution(
|
|
136
|
+
expression: ts.Expression,
|
|
137
|
+
): ExpressionResolution | undefined {
|
|
138
|
+
if (ts.isStringLiteral(expression)) return {
|
|
139
|
+
status: 'static', sourceKind: 'string_literal',
|
|
140
|
+
value: expression.text, rawExpression: safeRaw(expression),
|
|
141
|
+
placeholderKeys: [], evidence: ['string_literal'],
|
|
142
|
+
};
|
|
143
|
+
if (!ts.isNoSubstitutionTemplateLiteral(expression)) return undefined;
|
|
144
|
+
return {
|
|
145
|
+
status: 'static', sourceKind: 'no_substitution_template',
|
|
146
|
+
value: expression.text, rawExpression: safeRaw(expression),
|
|
147
|
+
placeholderKeys: [], evidence: ['no_substitution_template'],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function templateResolution(
|
|
152
|
+
expression: ts.TemplateExpression,
|
|
153
|
+
policy: 'operation_path' | 'external' | 'literal',
|
|
154
|
+
): ExpressionResolution {
|
|
155
|
+
const placeholderKeys = expression.templateSpans.map((span) =>
|
|
156
|
+
span.expression.getText(expression.getSourceFile()));
|
|
157
|
+
if (policy === 'operation_path') return {
|
|
158
|
+
status: 'dynamic',
|
|
159
|
+
sourceKind: 'template_with_substitutions',
|
|
160
|
+
value: stripQuotes(expression.getText(expression.getSourceFile())),
|
|
161
|
+
rawExpression: safeRaw(expression),
|
|
162
|
+
placeholderKeys,
|
|
163
|
+
evidence: ['operation_path_template_placeholders_retained'],
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
status: 'dynamic',
|
|
167
|
+
sourceKind: 'template_with_substitutions',
|
|
168
|
+
placeholderKeys,
|
|
169
|
+
evidence: ['template_substitutions_not_static_external_target'],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function identifierResolution(
|
|
174
|
+
expression: ts.Identifier,
|
|
175
|
+
use: ts.Node,
|
|
176
|
+
policy: 'operation_path' | 'external' | 'literal',
|
|
177
|
+
depth: number,
|
|
178
|
+
seen: Set<ts.Node>,
|
|
179
|
+
): ExpressionResolution {
|
|
180
|
+
if (depth >= maxAliasDepth) return {
|
|
181
|
+
status: 'unknown', sourceKind: 'const_alias',
|
|
182
|
+
rawExpression: safeRaw(expression), placeholderKeys: [],
|
|
183
|
+
evidence: ['alias_depth_exceeded'], constName: expression.text,
|
|
184
|
+
};
|
|
185
|
+
const binding = resolveBinding(expression, use);
|
|
186
|
+
if (!binding.declaration || !binding.initializer || !binding.immutable)
|
|
187
|
+
return {
|
|
188
|
+
status: 'dynamic', sourceKind: 'dynamic_expression',
|
|
189
|
+
rawExpression: safeRaw(expression), placeholderKeys: [],
|
|
190
|
+
evidence: binding.evidence, constName: expression.text,
|
|
191
|
+
};
|
|
192
|
+
if (seen.has(binding.declaration)) return {
|
|
193
|
+
status: 'unknown', sourceKind: 'const_alias',
|
|
194
|
+
rawExpression: safeRaw(expression), placeholderKeys: [],
|
|
195
|
+
evidence: ['alias_cycle_detected'], constName: expression.text,
|
|
196
|
+
};
|
|
197
|
+
seen.add(binding.declaration);
|
|
198
|
+
const resolved = resolveExpression(
|
|
199
|
+
binding.initializer, binding.declaration, policy, depth + 1, seen,
|
|
200
|
+
);
|
|
201
|
+
return {
|
|
202
|
+
...resolved,
|
|
203
|
+
sourceKind: 'const_alias',
|
|
204
|
+
rawExpression: safeRaw(expression),
|
|
205
|
+
constName: expression.text,
|
|
206
|
+
evidence: [...binding.evidence, ...resolved.evidence],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function resolveExpression(
|
|
211
|
+
expression: ts.Expression | undefined,
|
|
212
|
+
use: ts.Node,
|
|
213
|
+
policy: 'operation_path' | 'external' | 'literal',
|
|
214
|
+
depth = 0,
|
|
215
|
+
seen = new Set<ts.Node>(),
|
|
216
|
+
): ExpressionResolution {
|
|
217
|
+
if (!expression) return {
|
|
218
|
+
status: 'unknown', sourceKind: 'dynamic_expression',
|
|
219
|
+
placeholderKeys: [], evidence: ['expression_missing'],
|
|
220
|
+
};
|
|
221
|
+
const literal = staticResolution(expression);
|
|
222
|
+
if (literal) return literal;
|
|
223
|
+
if (ts.isTemplateExpression(expression))
|
|
224
|
+
return templateResolution(expression, policy);
|
|
225
|
+
if (ts.isIdentifier(expression))
|
|
226
|
+
return identifierResolution(expression, use, policy, depth, seen);
|
|
227
|
+
return {
|
|
228
|
+
status: 'dynamic', sourceKind: 'dynamic_expression',
|
|
229
|
+
rawExpression: safeRaw(expression), placeholderKeys: [],
|
|
230
|
+
evidence: [
|
|
231
|
+
`unsupported_${ts.SyntaxKind[expression.kind] ?? 'expression'}`,
|
|
232
|
+
],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function staticExpressionText(
|
|
237
|
+
expression: ts.Expression | undefined,
|
|
238
|
+
initializers: Map<string, ts.Expression>,
|
|
239
|
+
): string | undefined {
|
|
240
|
+
if (!expression) return undefined;
|
|
241
|
+
if (isStringLike(expression)) return expression.text;
|
|
242
|
+
if (ts.isIdentifier(expression) && initializers.has(expression.text))
|
|
243
|
+
return staticExpressionText(initializers.get(expression.text), initializers);
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function operationPathFromStatic(
|
|
248
|
+
text: string | undefined,
|
|
249
|
+
): string | undefined {
|
|
250
|
+
return text ? `/${stripQuotes(text).replace(/^\//, '')}` : undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function destinationExpressionShape(
|
|
254
|
+
expression: ts.Expression | undefined,
|
|
255
|
+
): string | undefined {
|
|
256
|
+
if (!expression) return undefined;
|
|
257
|
+
if (ts.isIdentifier(expression)) return 'identifier';
|
|
258
|
+
if (ts.isPropertyAccessExpression(expression)
|
|
259
|
+
|| ts.isElementAccessExpression(expression)) return 'property_read';
|
|
260
|
+
if (ts.isCallExpression(expression)) return 'function_call';
|
|
261
|
+
if (ts.isConditionalExpression(expression)) return 'conditional';
|
|
262
|
+
if (ts.isBinaryExpression(expression)) return 'binary_expression';
|
|
263
|
+
if (ts.isTemplateExpression(expression)) return 'template_expression';
|
|
264
|
+
return ts.SyntaxKind[expression.kind] ?? 'expression';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function staticConditionalCandidates(
|
|
268
|
+
expression: ts.Expression | undefined,
|
|
269
|
+
initializers: Map<string, ts.Expression>,
|
|
270
|
+
): string[] | undefined {
|
|
271
|
+
const resolved = expression && ts.isIdentifier(expression)
|
|
272
|
+
&& initializers.has(expression.text)
|
|
273
|
+
? initializers.get(expression.text) : expression;
|
|
274
|
+
if (!resolved || !ts.isConditionalExpression(resolved)) return undefined;
|
|
275
|
+
const left = staticExpressionText(resolved.whenTrue, initializers);
|
|
276
|
+
const right = staticExpressionText(resolved.whenFalse, initializers);
|
|
277
|
+
return left && right ? [...new Set([left, right])] : undefined;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function propertyInitializer(
|
|
281
|
+
object: ts.ObjectLiteralExpression,
|
|
282
|
+
key: string,
|
|
283
|
+
): ts.Expression | undefined {
|
|
284
|
+
for (const property of object.properties) {
|
|
285
|
+
if (ts.isPropertyAssignment(property)
|
|
286
|
+
&& nameOfProperty(property.name) === key) return property.initializer;
|
|
287
|
+
if (ts.isShorthandPropertyAssignment(property)
|
|
288
|
+
&& property.name.text === key) return property.name;
|
|
289
|
+
}
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function httpMethodFromObject(
|
|
294
|
+
object: ts.ObjectLiteralExpression,
|
|
295
|
+
use: ts.Node,
|
|
296
|
+
): string | undefined {
|
|
297
|
+
const text = resolveExpression(
|
|
298
|
+
propertyInitializer(object, 'method'), use, 'literal',
|
|
299
|
+
).value;
|
|
300
|
+
return text ? stripQuotes(text).toUpperCase() : undefined;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export const supportedHttpMethods = new Set([
|
|
304
|
+
'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD',
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
export function safeOperationName(
|
|
308
|
+
value: string | undefined,
|
|
309
|
+
): string | undefined {
|
|
310
|
+
if (!value || !/^[A-Za-z_$][\w$]*(?:[./][A-Za-z_$][\w$]*)*$/.test(value))
|
|
311
|
+
return undefined;
|
|
312
|
+
return operationPathFromStatic(value);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function wrapperSourceKind(sourceKind: string): string {
|
|
316
|
+
if (sourceKind.includes('const_alias')) return 'const';
|
|
317
|
+
if (sourceKind.includes('template')) return 'template';
|
|
318
|
+
if (sourceKind.includes('string_literal')) return 'literal';
|
|
319
|
+
return sourceKind.includes('conditional') ? 'ambiguous' : 'dynamic';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function literalPathSource(
|
|
323
|
+
analysis: OperationPathAnalysis,
|
|
324
|
+
): string | undefined {
|
|
325
|
+
if (analysis.status !== 'static') return undefined;
|
|
326
|
+
if (analysis.sourceKind.includes('const_alias'))
|
|
327
|
+
return 'same_scope_const_initializer';
|
|
328
|
+
if (analysis.sourceKind.includes('no_substitution_template'))
|
|
329
|
+
return 'template';
|
|
330
|
+
return analysis.sourceKind.includes('string_literal')
|
|
331
|
+
? 'literal' : analysis.sourceKind;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function legacyPathCandidates(
|
|
335
|
+
analysis: OperationPathAnalysis,
|
|
336
|
+
): Record<string, unknown> | undefined {
|
|
337
|
+
if (analysis.candidateRawPaths.length < 2
|
|
338
|
+
&& analysis.dynamicReassignments.length === 0) return undefined;
|
|
339
|
+
return {
|
|
340
|
+
candidatePaths: analysis.candidateRawPaths,
|
|
341
|
+
normalizedCandidateOperations: analysis.candidateNormalizedOperationPaths
|
|
342
|
+
.map((value) => value.replace(/^\//, '')),
|
|
343
|
+
candidateSourceKind: analysis.sourceKind,
|
|
344
|
+
candidateIdentifier: analysis.candidateIdentifier,
|
|
345
|
+
hasDynamicAssignments: analysis.dynamicReassignments.length > 0,
|
|
346
|
+
conservativeReason: analysis.dynamicReassignments.length > 0
|
|
347
|
+
? 'dynamic_assignment_observed' : 'candidate_tie',
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function hasTemplatePlaceholder(value: string): boolean {
|
|
352
|
+
return /\$\{|%7B|%7D/i.test(value);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function urlTargetFromExpression(
|
|
356
|
+
expression: ts.Expression | undefined,
|
|
357
|
+
use: ts.Node,
|
|
358
|
+
): Record<string, unknown> {
|
|
359
|
+
const resolved = resolveExpression(expression, use, 'external');
|
|
360
|
+
if (resolved.status === 'static' && resolved.value
|
|
361
|
+
&& !hasTemplatePlaceholder(resolved.value)) return {
|
|
362
|
+
kind: 'static_url', expression: resolved.value, dynamic: false,
|
|
363
|
+
sourceKind: resolved.sourceKind,
|
|
364
|
+
};
|
|
365
|
+
if (expression) return {
|
|
366
|
+
kind: 'url_expression', dynamic: true,
|
|
367
|
+
expression: `${resolved.sourceKind}:${resolved.placeholderKeys.join('|')}`,
|
|
368
|
+
expressionShape: resolved.sourceKind,
|
|
369
|
+
placeholderKeys: resolved.placeholderKeys,
|
|
370
|
+
};
|
|
371
|
+
return { kind: 'unknown', dynamic: false };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function destinationTargetFromExpression(
|
|
375
|
+
expression: ts.Expression | undefined,
|
|
376
|
+
use: ts.Node,
|
|
377
|
+
): Record<string, unknown> | undefined {
|
|
378
|
+
const resolved = resolveExpression(expression, use, 'external');
|
|
379
|
+
if (resolved.status === 'static' && resolved.value
|
|
380
|
+
&& !hasTemplatePlaceholder(resolved.value)) return {
|
|
381
|
+
kind: 'destination', expression: resolved.value, dynamic: false,
|
|
382
|
+
sourceKind: resolved.sourceKind,
|
|
383
|
+
};
|
|
384
|
+
const candidates = staticConditionalCandidates(
|
|
385
|
+
expression, new Map<string, ts.Expression>(),
|
|
386
|
+
);
|
|
387
|
+
if (candidates) return {
|
|
388
|
+
kind: 'destination', dynamic: true,
|
|
389
|
+
expressionShape: 'conditional', candidateLiterals: candidates,
|
|
390
|
+
};
|
|
391
|
+
const shape = destinationExpressionShape(expression);
|
|
392
|
+
return shape
|
|
393
|
+
? { kind: 'destination', dynamic: true, expressionShape: shape }
|
|
394
|
+
: undefined;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export interface ExternalHttpEvidence {
|
|
398
|
+
method?: string;
|
|
399
|
+
externalTarget: Record<string, unknown>;
|
|
400
|
+
classifier: string;
|
|
401
|
+
sourceCallShape: string;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function destinationLookupEvidence(
|
|
405
|
+
node: ts.CallExpression,
|
|
406
|
+
): ExternalHttpEvidence | undefined {
|
|
407
|
+
const object = node.arguments[0];
|
|
408
|
+
if (!object || !ts.isObjectLiteralExpression(object)) return undefined;
|
|
409
|
+
const destination = destinationTargetFromExpression(
|
|
410
|
+
propertyInitializer(object, 'destinationName'), node,
|
|
411
|
+
);
|
|
412
|
+
return {
|
|
413
|
+
externalTarget: destination ?? { kind: 'unknown', dynamic: false },
|
|
414
|
+
classifier: 'sap_destination_lookup',
|
|
415
|
+
sourceCallShape: 'useOrFetchDestination',
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function executeHttpEvidence(node: ts.CallExpression): ExternalHttpEvidence {
|
|
420
|
+
const destination = destinationTargetFromExpression(node.arguments[0], node);
|
|
421
|
+
const config = node.arguments[1];
|
|
422
|
+
const object = config && ts.isObjectLiteralExpression(config)
|
|
423
|
+
? config : undefined;
|
|
424
|
+
const method = object ? httpMethodFromObject(object, node) : undefined;
|
|
425
|
+
const url = object
|
|
426
|
+
? urlTargetFromExpression(propertyInitializer(object, 'url'), node)
|
|
427
|
+
: { kind: 'unknown', dynamic: false };
|
|
428
|
+
return {
|
|
429
|
+
method,
|
|
430
|
+
externalTarget: destination ? { ...url, destination } : url,
|
|
431
|
+
classifier: 'sap_execute_http_request',
|
|
432
|
+
sourceCallShape: 'executeHttpRequest',
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function axiosEvidence(node: ts.CallExpression): ExternalHttpEvidence {
|
|
437
|
+
const config = node.arguments[0];
|
|
438
|
+
if (!config || !ts.isObjectLiteralExpression(config)) return {
|
|
439
|
+
externalTarget: { kind: 'unknown', dynamic: false },
|
|
440
|
+
classifier: 'axios_unknown_call',
|
|
441
|
+
sourceCallShape: 'axios(...)',
|
|
442
|
+
};
|
|
443
|
+
return {
|
|
444
|
+
method: httpMethodFromObject(config, node),
|
|
445
|
+
externalTarget: urlTargetFromExpression(
|
|
446
|
+
propertyInitializer(config, 'url'), node,
|
|
447
|
+
),
|
|
448
|
+
classifier: 'axios_config_call',
|
|
449
|
+
sourceCallShape: 'axios(config)',
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function fetchEvidence(node: ts.CallExpression): ExternalHttpEvidence {
|
|
454
|
+
const init = node.arguments[1];
|
|
455
|
+
const object = init && ts.isObjectLiteralExpression(init) ? init : undefined;
|
|
456
|
+
return {
|
|
457
|
+
method: object ? httpMethodFromObject(object, node) : undefined,
|
|
458
|
+
externalTarget: urlTargetFromExpression(node.arguments[0], node),
|
|
459
|
+
classifier: 'fetch_call',
|
|
460
|
+
sourceCallShape: 'fetch',
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function axiosMemberEvidence(
|
|
465
|
+
node: ts.CallExpression,
|
|
466
|
+
source: ts.SourceFile,
|
|
467
|
+
): ExternalHttpEvidence | undefined {
|
|
468
|
+
const expression = node.expression;
|
|
469
|
+
if (!ts.isPropertyAccessExpression(expression)
|
|
470
|
+
|| !['get', 'post', 'put', 'patch', 'delete', 'head']
|
|
471
|
+
.includes(expression.name.text)
|
|
472
|
+
|| expression.expression.getText(source) !== 'axios') return undefined;
|
|
473
|
+
return {
|
|
474
|
+
method: expression.name.text.toUpperCase(),
|
|
475
|
+
externalTarget: urlTargetFromExpression(node.arguments[0], node),
|
|
476
|
+
classifier: 'axios_member_call',
|
|
477
|
+
sourceCallShape: `axios.${expression.name.text}`,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function externalHttpEvidence(
|
|
482
|
+
node: ts.CallExpression,
|
|
483
|
+
source: ts.SourceFile,
|
|
484
|
+
): ExternalHttpEvidence | undefined {
|
|
485
|
+
const expression = node.expression.getText(source);
|
|
486
|
+
if (expression === 'useOrFetchDestination')
|
|
487
|
+
return destinationLookupEvidence(node);
|
|
488
|
+
if (expression === 'executeHttpRequest') return executeHttpEvidence(node);
|
|
489
|
+
if (expression === 'axios') return axiosEvidence(node);
|
|
490
|
+
if (expression === 'fetch') return fetchEvidence(node);
|
|
491
|
+
return axiosMemberEvidence(node, source);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function collectServiceVariables(source: ts.SourceFile): Set<string> {
|
|
495
|
+
const variables = new Set([
|
|
496
|
+
'cds', 'messaging', 'messageClient', 'eventClient',
|
|
497
|
+
]);
|
|
498
|
+
const visit = (node: ts.Node): void => {
|
|
499
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)
|
|
500
|
+
&& node.initializer
|
|
501
|
+
&& /cds\.connect\.(to|messaging)\s*\(/.test(
|
|
502
|
+
node.initializer.getText(source),
|
|
503
|
+
)) variables.add(node.name.text);
|
|
504
|
+
ts.forEachChild(node, visit);
|
|
505
|
+
};
|
|
506
|
+
visit(source);
|
|
507
|
+
return variables;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function receiverName(expression: ts.Expression): string | undefined {
|
|
511
|
+
if (ts.isIdentifier(expression)) return expression.text;
|
|
512
|
+
return ts.isPropertyAccessExpression(expression)
|
|
513
|
+
? expression.getText(expression.getSourceFile()) : undefined;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function rootReceiverName(
|
|
517
|
+
expression: ts.Expression,
|
|
518
|
+
): string | undefined {
|
|
519
|
+
if (ts.isIdentifier(expression)) return expression.text;
|
|
520
|
+
if (ts.isPropertyAccessExpression(expression))
|
|
521
|
+
return rootReceiverName(expression.expression);
|
|
522
|
+
if (ts.isCallExpression(expression))
|
|
523
|
+
return rootReceiverName(expression.expression);
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function isSupportedEventReceiver(
|
|
528
|
+
receiver: string | undefined,
|
|
529
|
+
rootReceiver: string | undefined,
|
|
530
|
+
serviceVariables: Set<string>,
|
|
531
|
+
): boolean {
|
|
532
|
+
const candidate = rootReceiver ?? receiver;
|
|
533
|
+
if (!candidate) return false;
|
|
534
|
+
if (candidate === 'cds' || serviceVariables.has(candidate)) return true;
|
|
535
|
+
if (receiver && serviceVariables.has(receiver)) return true;
|
|
536
|
+
return /^(srv|service|serviceClient|messaging|messageClient|eventClient)$/
|
|
537
|
+
.test(candidate);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export interface WrapperSpec {
|
|
541
|
+
clientIndex?: number;
|
|
542
|
+
clientName?: string;
|
|
543
|
+
pathIndex: number;
|
|
544
|
+
methodIndex?: number;
|
|
545
|
+
methodName?: string;
|
|
546
|
+
methodLiteral?: string;
|
|
547
|
+
nestedWrapperFunction?: string;
|
|
548
|
+
definitionLine: number;
|
|
549
|
+
internalStart: number;
|
|
550
|
+
internalEnd: number;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
interface WrapperSend {
|
|
554
|
+
client: string;
|
|
555
|
+
path: string;
|
|
556
|
+
method?: string;
|
|
557
|
+
methodLiteral?: string;
|
|
558
|
+
nestedWrapperFunction?: string;
|
|
559
|
+
start: number;
|
|
560
|
+
end: number;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export function calledWrapperNames(source: ts.SourceFile): Set<string> {
|
|
564
|
+
const names = new Set<string>();
|
|
565
|
+
const visit = (node: ts.Node): void => {
|
|
566
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression))
|
|
567
|
+
names.add(node.expression.text);
|
|
568
|
+
const expression = ts.isCallExpression(node) ? node.expression : undefined;
|
|
569
|
+
if (expression && ts.isCallExpression(expression)
|
|
570
|
+
&& ts.isIdentifier(expression.expression))
|
|
571
|
+
names.add(expression.expression.text);
|
|
572
|
+
ts.forEachChild(node, visit);
|
|
573
|
+
};
|
|
574
|
+
visit(source);
|
|
575
|
+
return names;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function directWrapperSend(
|
|
579
|
+
node: ts.CallExpression,
|
|
580
|
+
source: ts.SourceFile,
|
|
581
|
+
): WrapperSend | undefined {
|
|
582
|
+
const expression = node.expression;
|
|
583
|
+
if (!ts.isPropertyAccessExpression(expression)
|
|
584
|
+
|| expression.name.text !== 'send'
|
|
585
|
+
|| !ts.isIdentifier(expression.expression)) return undefined;
|
|
586
|
+
const object = node.arguments[0];
|
|
587
|
+
if (!object || !ts.isObjectLiteralExpression(object)) return undefined;
|
|
588
|
+
const path = propertyInitializer(object, 'path');
|
|
589
|
+
if (!path || !ts.isIdentifier(path)) return undefined;
|
|
590
|
+
const method = propertyInitializer(object, 'method');
|
|
591
|
+
return {
|
|
592
|
+
client: expression.expression.text,
|
|
593
|
+
path: path.text,
|
|
594
|
+
method: method && ts.isIdentifier(method) ? method.text : undefined,
|
|
595
|
+
methodLiteral: resolveExpression(method, node, 'literal').value,
|
|
596
|
+
start: node.getStart(source),
|
|
597
|
+
end: node.getEnd(),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function nestedWrapperSend(
|
|
602
|
+
node: ts.CallExpression,
|
|
603
|
+
source: ts.SourceFile,
|
|
604
|
+
specs: Map<string, WrapperSpec>,
|
|
605
|
+
): WrapperSend | undefined {
|
|
606
|
+
if (!ts.isIdentifier(node.expression)) return undefined;
|
|
607
|
+
const nested = specs.get(node.expression.text);
|
|
608
|
+
if (!nested) return undefined;
|
|
609
|
+
const path = node.arguments[nested.pathIndex];
|
|
610
|
+
const client = nested.clientIndex === undefined
|
|
611
|
+
? undefined : node.arguments[nested.clientIndex];
|
|
612
|
+
const pathName = path && ts.isIdentifier(path) ? path.text : undefined;
|
|
613
|
+
const clientName = client && ts.isIdentifier(client)
|
|
614
|
+
? client.text : nested.clientName;
|
|
615
|
+
if (!pathName || !clientName) return undefined;
|
|
616
|
+
return {
|
|
617
|
+
client: clientName,
|
|
618
|
+
path: pathName,
|
|
619
|
+
method: nested.methodName,
|
|
620
|
+
methodLiteral: nested.methodLiteral,
|
|
621
|
+
nestedWrapperFunction: node.expression.text,
|
|
622
|
+
start: node.getStart(source),
|
|
623
|
+
end: node.getEnd(),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function wrapperSends(
|
|
628
|
+
fn: ts.FunctionLikeDeclaration,
|
|
629
|
+
source: ts.SourceFile,
|
|
630
|
+
specs: Map<string, WrapperSpec>,
|
|
631
|
+
): WrapperSend[] {
|
|
632
|
+
const sends: WrapperSend[] = [];
|
|
633
|
+
const visit = (node: ts.Node): void => {
|
|
634
|
+
if (ts.isCallExpression(node)) {
|
|
635
|
+
const send = directWrapperSend(node, source)
|
|
636
|
+
?? nestedWrapperSend(node, source, specs);
|
|
637
|
+
if (send) sends.push(send);
|
|
638
|
+
}
|
|
639
|
+
ts.forEachChild(node, visit);
|
|
640
|
+
};
|
|
641
|
+
visit(fn);
|
|
642
|
+
return sends;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function isExportedWrapper(fn: ts.FunctionLikeDeclaration): boolean {
|
|
646
|
+
const declaration = ts.isFunctionDeclaration(fn)
|
|
647
|
+
? fn
|
|
648
|
+
: ts.isVariableDeclaration(fn.parent)
|
|
649
|
+
? fn.parent.parent.parent : undefined;
|
|
650
|
+
if (!declaration || !ts.canHaveModifiers(declaration)) return false;
|
|
651
|
+
return ts.getModifiers(declaration)?.some((modifier) =>
|
|
652
|
+
modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function optionalIndex(index: number): number | undefined {
|
|
656
|
+
return index >= 0 ? index : undefined;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function knownWrapperClient(
|
|
660
|
+
client: string,
|
|
661
|
+
serviceVariables: Set<string>,
|
|
662
|
+
): boolean {
|
|
663
|
+
if (serviceVariables.has(client)) return true;
|
|
664
|
+
return /^(srv|service|serviceClient|client|.*Client)$/.test(client);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function wrapperSpec(
|
|
668
|
+
name: string,
|
|
669
|
+
fn: ts.FunctionLikeDeclaration,
|
|
670
|
+
source: ts.SourceFile,
|
|
671
|
+
calledNames: Set<string>,
|
|
672
|
+
serviceVariables: Set<string>,
|
|
673
|
+
specs: Map<string, WrapperSpec>,
|
|
674
|
+
): WrapperSpec | undefined {
|
|
675
|
+
if (!calledNames.has(name) && !isExportedWrapper(fn)) return undefined;
|
|
676
|
+
const sends = wrapperSends(fn, source, specs);
|
|
677
|
+
if (sends.length !== 1) return undefined;
|
|
678
|
+
const found = sends[0];
|
|
679
|
+
if (!found) return undefined;
|
|
680
|
+
const parameters = fn.parameters.map((parameter) =>
|
|
681
|
+
ts.isIdentifier(parameter.name) ? parameter.name.text : undefined);
|
|
682
|
+
const clientIndex = parameters.indexOf(found.client);
|
|
683
|
+
const pathIndex = parameters.indexOf(found.path);
|
|
684
|
+
const methodIndex = parameters.indexOf(found.method ?? '');
|
|
685
|
+
if (pathIndex < 0) return undefined;
|
|
686
|
+
if (clientIndex < 0
|
|
687
|
+
&& !knownWrapperClient(found.client, serviceVariables)) return undefined;
|
|
688
|
+
return {
|
|
689
|
+
clientIndex: optionalIndex(clientIndex),
|
|
690
|
+
clientName: clientIndex >= 0 ? undefined : found.client,
|
|
691
|
+
pathIndex,
|
|
692
|
+
methodIndex: optionalIndex(methodIndex),
|
|
693
|
+
methodName: found.method,
|
|
694
|
+
methodLiteral: found.methodLiteral,
|
|
695
|
+
nestedWrapperFunction: found.nestedWrapperFunction,
|
|
696
|
+
definitionLine: lineOf(source.text, fn.getStart(source)),
|
|
697
|
+
internalStart: found.start,
|
|
698
|
+
internalEnd: found.end,
|
|
699
|
+
};
|
|
700
|
+
}
|