@saptools/service-flow 0.1.50 → 0.1.52
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 +14 -0
- package/README.md +16 -3
- package/TECHNICAL-NOTE.md +10 -1
- package/dist/{chunk-52OUS3MO.js → chunk-PTLDSHRC.js} +2663 -363
- package/dist/chunk-PTLDSHRC.js.map +1 -0
- package/dist/cli.js +318 -510
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +59 -17
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/000-clean.ts +82 -0
- package/src/cli.ts +97 -57
- package/src/db/connection.ts +1 -1
- package/src/db/migrations.ts +5 -3
- package/src/db/repositories.ts +120 -22
- package/src/db/schema.ts +7 -2
- package/src/index.ts +1 -1
- package/src/indexer/repository-indexer.ts +57 -29
- package/src/indexer/workspace-indexer.ts +84 -6
- package/src/linker/cross-repo-linker.ts +22 -2
- package/src/linker/service-resolver.ts +15 -4
- package/src/output/table-output.ts +56 -3
- package/src/parsers/cds-parser.ts +8 -2
- package/src/parsers/decorator-parser.ts +382 -48
- package/src/parsers/handler-registration-parser.ts +38 -21
- package/src/parsers/imported-wrapper-parser.ts +18 -5
- package/src/parsers/outbound-call-parser.ts +25 -8
- package/src/parsers/package-json-parser.ts +36 -11
- package/src/parsers/service-binding-parser-helpers.ts +8 -1
- package/src/parsers/service-binding-parser.ts +6 -3
- package/src/parsers/symbol-parser.ts +13 -3
- package/src/parsers/ts-project.ts +54 -0
- package/src/trace/000-dynamic-target-types.ts +84 -0
- package/src/trace/001-dynamic-identity.ts +280 -0
- package/src/trace/002-trace-diagnostics.ts +54 -0
- package/src/trace/003-dynamic-references.ts +82 -0
- package/src/trace/dynamic-branches.ts +45 -0
- package/src/trace/dynamic-targets.ts +654 -0
- package/src/trace/evidence.ts +391 -22
- package/src/trace/selectors.ts +483 -0
- package/src/trace/trace-engine.ts +101 -94
- package/src/types.ts +39 -1
- package/dist/chunk-52OUS3MO.js.map +0 -1
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import ts from 'typescript';
|
|
5
5
|
import type { HandlerRegistrationFact } from '../types.js';
|
|
6
6
|
import { normalizePath } from '../utils/path-utils.js';
|
|
7
|
+
import type { RepositorySourceContext } from './ts-project.js';
|
|
7
8
|
|
|
8
9
|
interface ImportEvidence { importedName: string; source: string }
|
|
9
10
|
interface ClassEvidence { className: string; importSource?: string }
|
|
@@ -26,16 +27,26 @@ function importSourceFor(identifier: string, imports: Map<string, ImportEvidence
|
|
|
26
27
|
return evidence ? `${evidence.source}#${evidence.importedName}` : undefined;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
export async function parseHandlerRegistrations(
|
|
30
|
+
export async function parseHandlerRegistrations(
|
|
31
|
+
repoPath: string,
|
|
32
|
+
filePath: string,
|
|
33
|
+
context?: RepositorySourceContext,
|
|
34
|
+
): Promise<HandlerRegistrationFact[]> {
|
|
30
35
|
const absolutePath = path.join(repoPath, filePath);
|
|
31
|
-
const
|
|
32
|
-
const
|
|
36
|
+
const snapshot = context?.get(filePath);
|
|
37
|
+
const text = snapshot?.text ?? await fs.readFile(absolutePath, 'utf8');
|
|
38
|
+
const sourceFile = snapshot?.sourceFile() ?? ts.createSourceFile(
|
|
39
|
+
filePath, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS,
|
|
40
|
+
);
|
|
33
41
|
const imports = collectImports(sourceFile);
|
|
34
|
-
const localArrays = collectLocalArrays(
|
|
42
|
+
const localArrays = collectLocalArrays(
|
|
43
|
+
sourceFile, imports, new Map(), repoPath, filePath, context,
|
|
44
|
+
);
|
|
35
45
|
const out: HandlerRegistrationFact[] = [];
|
|
36
|
-
|
|
37
46
|
function emitFromExpression(expression: ts.Expression, call: ts.CallExpression): void {
|
|
38
|
-
const classes = resolveArrayExpression(
|
|
47
|
+
const classes = resolveArrayExpression(
|
|
48
|
+
expression, localArrays, imports, repoPath, filePath, new Set(), context,
|
|
49
|
+
);
|
|
39
50
|
for (const cls of classes) {
|
|
40
51
|
out.push({
|
|
41
52
|
className: cls.className,
|
|
@@ -55,7 +66,6 @@ export async function parseHandlerRegistrations(repoPath: string, filePath: stri
|
|
|
55
66
|
});
|
|
56
67
|
}
|
|
57
68
|
}
|
|
58
|
-
|
|
59
69
|
function visit(node: ts.Node): void {
|
|
60
70
|
if (ts.isCallExpression(node) && isRegistrationCall(node)) {
|
|
61
71
|
const handlerExpr = handlerExpression(node, sourceFile);
|
|
@@ -98,45 +108,47 @@ function collectImports(sourceFile: ts.SourceFile): Map<string, ImportEvidence>
|
|
|
98
108
|
}
|
|
99
109
|
return imports;
|
|
100
110
|
}
|
|
101
|
-
function collectLocalArrays(sourceFile: ts.SourceFile, imports: Map<string, ImportEvidence>, seed: Map<string, ClassEvidence[]>, repoPath = '', fromFile = ''): Map<string, ClassEvidence[]> {
|
|
111
|
+
function collectLocalArrays(sourceFile: ts.SourceFile, imports: Map<string, ImportEvidence>, seed: Map<string, ClassEvidence[]>, repoPath = '', fromFile = '', context?: RepositorySourceContext): Map<string, ClassEvidence[]> {
|
|
102
112
|
const arrays = new Map(seed);
|
|
103
113
|
for (const statement of sourceFile.statements) {
|
|
104
114
|
if (ts.isVariableStatement(statement)) {
|
|
105
115
|
for (const decl of statement.declarationList.declarations) {
|
|
106
116
|
if (ts.isIdentifier(decl.name) && decl.initializer && ts.isArrayLiteralExpression(decl.initializer)) {
|
|
107
|
-
arrays.set(decl.name.text, resolveArrayLiteral(
|
|
117
|
+
arrays.set(decl.name.text, resolveArrayLiteral(
|
|
118
|
+
decl.initializer, arrays, imports, repoPath, fromFile, new Set(), context,
|
|
119
|
+
));
|
|
108
120
|
}
|
|
109
121
|
}
|
|
110
122
|
}
|
|
111
123
|
}
|
|
112
124
|
return arrays;
|
|
113
125
|
}
|
|
114
|
-
function resolveArrayExpression(expr: ts.Expression, arrays: Map<string, ClassEvidence[]>, imports: Map<string, ImportEvidence>, repoPath: string, fromFile: string, seen: Set<string
|
|
115
|
-
if (ts.isArrayLiteralExpression(expr)) return resolveArrayLiteral(expr, arrays, imports, repoPath, fromFile, seen);
|
|
126
|
+
function resolveArrayExpression(expr: ts.Expression, arrays: Map<string, ClassEvidence[]>, imports: Map<string, ImportEvidence>, repoPath: string, fromFile: string, seen: Set<string>, context?: RepositorySourceContext): ClassEvidence[] {
|
|
127
|
+
if (ts.isArrayLiteralExpression(expr)) return resolveArrayLiteral(expr, arrays, imports, repoPath, fromFile, seen, context);
|
|
116
128
|
if (ts.isIdentifier(expr)) {
|
|
117
129
|
const local = arrays.get(expr.text);
|
|
118
130
|
if (local) return local;
|
|
119
131
|
const evidence = imports.get(expr.text);
|
|
120
|
-
if (evidence && isRelative(evidence.source)) return resolveImportedArray(repoPath, fromFile, evidence, seen);
|
|
132
|
+
if (evidence && isRelative(evidence.source)) return resolveImportedArray(repoPath, fromFile, evidence, seen, context);
|
|
121
133
|
if (evidence) return [{ className: evidence.importedName === 'default' ? expr.text : evidence.importedName, importSource: `${evidence.source}#${evidence.importedName}` }];
|
|
122
134
|
}
|
|
123
135
|
return [];
|
|
124
136
|
}
|
|
125
|
-
function resolveArrayLiteral(array: ts.ArrayLiteralExpression, arrays: Map<string, ClassEvidence[]>, imports: Map<string, ImportEvidence>, repoPath: string, fromFile: string, seen: Set<string
|
|
137
|
+
function resolveArrayLiteral(array: ts.ArrayLiteralExpression, arrays: Map<string, ClassEvidence[]>, imports: Map<string, ImportEvidence>, repoPath: string, fromFile: string, seen: Set<string>, context?: RepositorySourceContext): ClassEvidence[] {
|
|
126
138
|
const out: ClassEvidence[] = [];
|
|
127
139
|
for (const element of array.elements) {
|
|
128
|
-
if (ts.isSpreadElement(element)) out.push(...resolveArrayExpression(element.expression, arrays, imports, repoPath, fromFile, seen));
|
|
140
|
+
if (ts.isSpreadElement(element)) out.push(...resolveArrayExpression(element.expression, arrays, imports, repoPath, fromFile, seen, context));
|
|
129
141
|
else if (ts.isIdentifier(element)) out.push({ className: element.text, importSource: importSourceFor(element.text, imports) });
|
|
130
142
|
}
|
|
131
143
|
return out;
|
|
132
144
|
}
|
|
133
|
-
function resolveImportedArray(repoPath: string, fromFile: string, evidence: ImportEvidence, seen: Set<string
|
|
145
|
+
function resolveImportedArray(repoPath: string, fromFile: string, evidence: ImportEvidence, seen: Set<string>, context?: RepositorySourceContext): ClassEvidence[] {
|
|
134
146
|
const moduleFile = resolveRelativeModule(repoPath, fromFile, evidence.source);
|
|
135
147
|
if (!moduleFile) return [];
|
|
136
148
|
const key = `${moduleFile}:${evidence.importedName}`;
|
|
137
149
|
if (seen.has(key) || seen.size > MAX_EXPORT_DEPTH) return [];
|
|
138
150
|
seen.add(key);
|
|
139
|
-
const exports = readExports(repoPath, moduleFile, seen);
|
|
151
|
+
const exports = readExports(repoPath, moduleFile, seen, context);
|
|
140
152
|
if (evidence.importedName === 'default') return exports.defaultArray ?? [];
|
|
141
153
|
return exports.arrays.get(evidence.importedName) ?? exports.arrays.get(exports.aliases.get(evidence.importedName) ?? evidence.importedName) ?? [];
|
|
142
154
|
}
|
|
@@ -150,13 +162,16 @@ function resolveRelativeModule(repoPath: string, fromFile: string, specifier: st
|
|
|
150
162
|
}
|
|
151
163
|
return undefined;
|
|
152
164
|
}
|
|
153
|
-
function readExports(repoPath: string, filePath: string, seen: Set<string
|
|
165
|
+
function readExports(repoPath: string, filePath: string, seen: Set<string>, context?: RepositorySourceContext): FileExports {
|
|
154
166
|
const absolute = path.join(repoPath, filePath);
|
|
155
167
|
let text: string;
|
|
156
|
-
|
|
157
|
-
|
|
168
|
+
const snapshot = context?.get(filePath);
|
|
169
|
+
try { text = snapshot?.text ?? fsSync.readFileSync(absolute, 'utf8'); } catch { return { arrays: new Map(), aliases: new Map() }; }
|
|
170
|
+
const sourceFile = snapshot?.sourceFile() ?? ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
158
171
|
const imports = collectImports(sourceFile);
|
|
159
|
-
const arrays = collectLocalArrays(
|
|
172
|
+
const arrays = collectLocalArrays(
|
|
173
|
+
sourceFile, imports, new Map(), repoPath, filePath, context,
|
|
174
|
+
);
|
|
160
175
|
const aliases = new Map<string, string>();
|
|
161
176
|
let defaultArray: ClassEvidence[] | undefined;
|
|
162
177
|
for (const statement of sourceFile.statements) {
|
|
@@ -167,7 +182,9 @@ function readExports(repoPath: string, filePath: string, seen: Set<string>): Fil
|
|
|
167
182
|
const local = element.propertyName?.text ?? element.name.text;
|
|
168
183
|
aliases.set(element.name.text, local);
|
|
169
184
|
if (module && isRelative(module)) {
|
|
170
|
-
const imported = resolveImportedArray(
|
|
185
|
+
const imported = resolveImportedArray(
|
|
186
|
+
repoPath, filePath, { source: module, importedName: local }, seen, context,
|
|
187
|
+
);
|
|
171
188
|
if (imported.length > 0) arrays.set(element.name.text, imported);
|
|
172
189
|
}
|
|
173
190
|
}
|
|
@@ -4,6 +4,7 @@ import { classifyODataPathIntent } from '../linker/odata-path-normalizer.js';
|
|
|
4
4
|
import type { OutboundCallFact } from '../types.js';
|
|
5
5
|
import { normalizePath } from '../utils/path-utils.js';
|
|
6
6
|
import { importsFor, lineOf, readSource, type ImportBinding } from './service-binding-parser-helpers.js';
|
|
7
|
+
import type { RepositorySourceContext } from './ts-project.js';
|
|
7
8
|
import {
|
|
8
9
|
analyzeOperationPath,
|
|
9
10
|
operationPathExpression,
|
|
@@ -25,6 +26,7 @@ export async function parseImportedWrapperCalls(
|
|
|
25
26
|
filePath: string,
|
|
26
27
|
source: ts.SourceFile,
|
|
27
28
|
serviceBindings: Set<string>,
|
|
29
|
+
context?: RepositorySourceContext,
|
|
28
30
|
): Promise<OutboundCallFact[]> {
|
|
29
31
|
const imports = await importsFor(repoPath, filePath, source);
|
|
30
32
|
const importedByLocal = new Map(imports.filter((item) => item.sourceFile).map((item) => [item.localName, item]));
|
|
@@ -35,7 +37,7 @@ export async function parseImportedWrapperCalls(
|
|
|
35
37
|
if (!ts.isIdentifier(call.expression)) continue;
|
|
36
38
|
const imported = importedByLocal.get(call.expression.text);
|
|
37
39
|
if (!imported?.sourceFile) continue;
|
|
38
|
-
const spec = await loadWrapperSpec(repoPath, imported, cache, 0);
|
|
40
|
+
const spec = await loadWrapperSpec(repoPath, imported, cache, 0, context);
|
|
39
41
|
const fact = spec ? wrapperCallFact(source, filePath, call, spec, serviceBindings) : undefined;
|
|
40
42
|
if (fact) out.push(fact);
|
|
41
43
|
}
|
|
@@ -57,12 +59,15 @@ async function loadWrapperSpec(
|
|
|
57
59
|
imported: ImportBinding,
|
|
58
60
|
cache: Map<string, Promise<WrapperSpec | undefined>>,
|
|
59
61
|
depth: number,
|
|
62
|
+
context?: RepositorySourceContext,
|
|
60
63
|
): Promise<WrapperSpec | undefined> {
|
|
61
64
|
if (!imported.sourceFile || depth > 5) return undefined;
|
|
62
65
|
const key = `${imported.sourceFile}#${imported.exportedName}`;
|
|
63
66
|
const existing = cache.get(key);
|
|
64
67
|
if (existing) return existing;
|
|
65
|
-
const pending = inspectWrapper(
|
|
68
|
+
const pending = inspectWrapper(
|
|
69
|
+
repoPath, imported.sourceFile, imported.exportedName, cache, depth, context,
|
|
70
|
+
);
|
|
66
71
|
cache.set(key, pending);
|
|
67
72
|
return pending;
|
|
68
73
|
}
|
|
@@ -73,14 +78,19 @@ async function inspectWrapper(
|
|
|
73
78
|
exportedName: string,
|
|
74
79
|
cache: Map<string, Promise<WrapperSpec | undefined>>,
|
|
75
80
|
depth: number,
|
|
81
|
+
context?: RepositorySourceContext,
|
|
76
82
|
): Promise<WrapperSpec | undefined> {
|
|
77
|
-
const source = await readSource(
|
|
83
|
+
const source = await readSource(
|
|
84
|
+
path.join(repoPath, sourceFile), context, sourceFile,
|
|
85
|
+
);
|
|
78
86
|
if (!source) return undefined;
|
|
79
87
|
const named = findFunction(source, exportedName);
|
|
80
88
|
if (!named) return undefined;
|
|
81
89
|
const direct = directSendSpec(source, sourceFile, named.name, named.fn);
|
|
82
90
|
if (direct) return direct;
|
|
83
|
-
return nestedSendSpec(
|
|
91
|
+
return nestedSendSpec(
|
|
92
|
+
repoPath, sourceFile, source, named.name, named.fn, cache, depth, context,
|
|
93
|
+
);
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
function directSendSpec(source: ts.SourceFile, sourceFile: string, name: string, fn: ts.FunctionLikeDeclaration): WrapperSpec | undefined {
|
|
@@ -113,6 +123,7 @@ async function nestedSendSpec(
|
|
|
113
123
|
fn: ts.FunctionLikeDeclaration,
|
|
114
124
|
cache: Map<string, Promise<WrapperSpec | undefined>>,
|
|
115
125
|
depth: number,
|
|
126
|
+
context?: RepositorySourceContext,
|
|
116
127
|
): Promise<WrapperSpec | undefined> {
|
|
117
128
|
const imports = await importsFor(repoPath, sourceFile, source);
|
|
118
129
|
const byLocal = new Map(imports.filter((item) => item.sourceFile).map((item) => [item.localName, item]));
|
|
@@ -123,7 +134,9 @@ async function nestedSendSpec(
|
|
|
123
134
|
if (calls.length !== 1) return undefined;
|
|
124
135
|
const call = calls[0];
|
|
125
136
|
const imported = call && ts.isIdentifier(call.expression) ? byLocal.get(call.expression.text) : undefined;
|
|
126
|
-
const nested = imported
|
|
137
|
+
const nested = imported
|
|
138
|
+
? await loadWrapperSpec(repoPath, imported, cache, depth + 1, context)
|
|
139
|
+
: undefined;
|
|
127
140
|
if (!call || !nested) return undefined;
|
|
128
141
|
const params = parameterNames(fn);
|
|
129
142
|
const clientIndex = mappedParameterIndex(call.arguments[nested.clientIndex], params);
|
|
@@ -8,6 +8,7 @@ import { summarizeExpression } from '../utils/redaction.js';
|
|
|
8
8
|
import { classifyODataPathIntent } from '../linker/odata-path-normalizer.js';
|
|
9
9
|
import { parseServiceBindings } from './service-binding-parser.js';
|
|
10
10
|
import { parseImportedWrapperCalls } from './imported-wrapper-parser.js';
|
|
11
|
+
import type { RepositorySourceContext } from './ts-project.js';
|
|
11
12
|
import {
|
|
12
13
|
analyzeOperationPath,
|
|
13
14
|
operationPathExpression,
|
|
@@ -510,15 +511,31 @@ export function containsSupportedOutboundCall(node: ts.Node): boolean {
|
|
|
510
511
|
export async function parseOutboundCalls(
|
|
511
512
|
repoPath: string,
|
|
512
513
|
filePath: string,
|
|
514
|
+
context?: RepositorySourceContext,
|
|
513
515
|
): Promise<OutboundCallFact[]> {
|
|
514
|
-
const
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const
|
|
516
|
+
const snapshot = context?.get(filePath);
|
|
517
|
+
const text = snapshot?.text
|
|
518
|
+
?? await fs.readFile(path.join(repoPath, filePath), 'utf8');
|
|
519
|
+
const source = snapshot?.sourceFile() ?? ts.createSourceFile(
|
|
520
|
+
filePath, text, ts.ScriptTarget.Latest, true,
|
|
521
|
+
filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS,
|
|
522
|
+
);
|
|
523
|
+
const bindingNames = new Set((await parseServiceBindings(
|
|
524
|
+
repoPath, filePath, context,
|
|
525
|
+
)).map((binding) => binding.variableName));
|
|
526
|
+
const importedWrappers = await parseImportedWrapperCalls(
|
|
527
|
+
repoPath, filePath, source, bindingNames, context,
|
|
528
|
+
);
|
|
529
|
+
return [...classifyOutboundCallsInSource(source, filePath).map((call) => call.fact), ...importedWrappers, ...parseLocalServiceCalls(text, filePath, source)];
|
|
530
|
+
}
|
|
531
|
+
function parseLocalServiceCalls(
|
|
532
|
+
text: string,
|
|
533
|
+
filePath: string,
|
|
534
|
+
source = ts.createSourceFile(
|
|
535
|
+
filePath, text, ts.ScriptTarget.Latest, true,
|
|
536
|
+
filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS,
|
|
537
|
+
),
|
|
538
|
+
): OutboundCallFact[] {
|
|
522
539
|
const aliases = new Map<string, { service: string; lookup: string; chain: string[] }>();
|
|
523
540
|
const calls: OutboundCallFact[] = [];
|
|
524
541
|
const visit = (node: ts.Node): void => {
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import type { CdsRequire, PackageFacts } from '../types.js';
|
|
4
|
+
interface ParsePackageJsonOptions {
|
|
5
|
+
strict?: boolean;
|
|
6
|
+
allowMissing?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface PackageJsonSnapshot {
|
|
9
|
+
facts: PackageFacts;
|
|
10
|
+
rawText: string;
|
|
11
|
+
}
|
|
12
|
+
function emptyPackageFacts(): PackageFacts {
|
|
13
|
+
return { dependencies: {}, cdsRequires: [], scripts: {} };
|
|
14
|
+
}
|
|
4
15
|
function recordOfString(value: unknown): Record<string, string> {
|
|
5
16
|
if (!value || typeof value !== 'object') return {};
|
|
6
17
|
return Object.fromEntries(
|
|
@@ -41,23 +52,37 @@ function readRequires(cds: unknown): CdsRequire[] {
|
|
|
41
52
|
});
|
|
42
53
|
}
|
|
43
54
|
export async function parsePackageJson(
|
|
44
|
-
repoPath: string
|
|
55
|
+
repoPath: string,
|
|
56
|
+
options: ParsePackageJsonOptions = {},
|
|
45
57
|
): Promise<PackageFacts> {
|
|
58
|
+
return (await loadPackageJsonSnapshot(repoPath, options)).facts;
|
|
59
|
+
}
|
|
60
|
+
export async function loadPackageJsonSnapshot(
|
|
61
|
+
repoPath: string,
|
|
62
|
+
options: ParsePackageJsonOptions = {},
|
|
63
|
+
): Promise<PackageJsonSnapshot> {
|
|
46
64
|
try {
|
|
47
65
|
const raw = await fs.readFile(path.join(repoPath, 'package.json'), 'utf8');
|
|
48
66
|
const json = JSON.parse(raw) as Record<string, unknown>;
|
|
49
67
|
return {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
typeof json.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
rawText: raw,
|
|
69
|
+
facts: {
|
|
70
|
+
packageName: typeof json.name === 'string' ? json.name : undefined,
|
|
71
|
+
packageVersion:
|
|
72
|
+
typeof json.version === 'string' ? json.version : undefined,
|
|
73
|
+
dependencies: {
|
|
74
|
+
...recordOfString(json.dependencies),
|
|
75
|
+
...recordOfString(json.devDependencies),
|
|
76
|
+
},
|
|
77
|
+
cdsRequires: readRequires(json.cds),
|
|
78
|
+
scripts: recordOfString(json.scripts),
|
|
56
79
|
},
|
|
57
|
-
cdsRequires: readRequires(json.cds),
|
|
58
|
-
scripts: recordOfString(json.scripts)
|
|
59
80
|
};
|
|
60
|
-
} catch {
|
|
61
|
-
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const missing = typeof error === 'object' && error !== null
|
|
83
|
+
&& 'code' in error && error.code === 'ENOENT';
|
|
84
|
+
if (!options.strict || (options.allowMissing && missing))
|
|
85
|
+
return { facts: emptyPackageFacts(), rawText: '' };
|
|
86
|
+
throw error;
|
|
62
87
|
}
|
|
63
88
|
}
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import ts from 'typescript';
|
|
4
4
|
import { normalizePath } from '../utils/path-utils.js';
|
|
5
|
+
import type { RepositorySourceContext } from './ts-project.js';
|
|
5
6
|
|
|
6
7
|
export interface HelperBinding {
|
|
7
8
|
exportedName: string;
|
|
@@ -123,7 +124,13 @@ export function findConnectInExpression(expr: ts.Expression): Omit<HelperBinding
|
|
|
123
124
|
return found;
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
export async function readSource(
|
|
127
|
+
export async function readSource(
|
|
128
|
+
abs: string,
|
|
129
|
+
context?: RepositorySourceContext,
|
|
130
|
+
filePath?: string,
|
|
131
|
+
): Promise<ts.SourceFile | undefined> {
|
|
132
|
+
const snapshot = filePath ? context?.get(filePath) : undefined;
|
|
133
|
+
if (snapshot) return snapshot.sourceFile();
|
|
127
134
|
try {
|
|
128
135
|
const text = await fs.readFile(abs, 'utf8');
|
|
129
136
|
return ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type HelperBinding,
|
|
16
16
|
type ImportBinding,
|
|
17
17
|
} from './service-binding-parser-helpers.js';
|
|
18
|
+
import type { RepositorySourceContext } from './ts-project.js';
|
|
18
19
|
|
|
19
20
|
function collectLocalBindingFacts(
|
|
20
21
|
fn: ts.FunctionLikeDeclaration,
|
|
@@ -125,8 +126,9 @@ function exportedLocalNames(sf: ts.SourceFile): Map<string, string> {
|
|
|
125
126
|
async function helperBindings(
|
|
126
127
|
repoPath: string,
|
|
127
128
|
filePath: string,
|
|
129
|
+
context?: RepositorySourceContext,
|
|
128
130
|
): Promise<HelperBinding[]> {
|
|
129
|
-
const sf = await readSource(path.join(repoPath, filePath));
|
|
131
|
+
const sf = await readSource(path.join(repoPath, filePath), context, filePath);
|
|
130
132
|
if (!sf) return [];
|
|
131
133
|
const sourceFileAst = sf;
|
|
132
134
|
const out: HelperBinding[] = [];
|
|
@@ -195,8 +197,9 @@ async function helperBindings(
|
|
|
195
197
|
export async function parseServiceBindings(
|
|
196
198
|
repoPath: string,
|
|
197
199
|
filePath: string,
|
|
200
|
+
context?: RepositorySourceContext,
|
|
198
201
|
): Promise<ServiceBindingFact[]> {
|
|
199
|
-
const sf = await readSource(path.join(repoPath, filePath));
|
|
202
|
+
const sf = await readSource(path.join(repoPath, filePath), context, filePath);
|
|
200
203
|
if (!sf) return [];
|
|
201
204
|
const sourceFileAst = sf;
|
|
202
205
|
const out: ServiceBindingFact[] = [];
|
|
@@ -237,7 +240,7 @@ export async function parseServiceBindings(
|
|
|
237
240
|
if (!helperCache.has(imp.sourceFile))
|
|
238
241
|
helperCache.set(
|
|
239
242
|
imp.sourceFile,
|
|
240
|
-
await helperBindings(repoPath, imp.sourceFile),
|
|
243
|
+
await helperBindings(repoPath, imp.sourceFile, context),
|
|
241
244
|
);
|
|
242
245
|
return (helperCache.get(imp.sourceFile) ?? [])
|
|
243
246
|
.filter((h) => h.exportedName === imp.exportedName)
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import ts from 'typescript';
|
|
4
4
|
import type { ExecutableSymbolFact, SymbolCallFact } from '../types.js';
|
|
5
5
|
import { containsSupportedOutboundCall } from './outbound-call-parser.js';
|
|
6
|
+
import type { RepositorySourceContext } from './ts-project.js';
|
|
6
7
|
import { normalizePath } from '../utils/path-utils.js';
|
|
7
8
|
|
|
8
9
|
function lineOf(source: ts.SourceFile, pos: number): number {
|
|
@@ -167,9 +168,18 @@ function parameterBindings(params: ts.NodeArray<ts.ParameterDeclaration>): Param
|
|
|
167
168
|
return [];
|
|
168
169
|
});
|
|
169
170
|
}
|
|
170
|
-
export async function parseExecutableSymbols(
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
export async function parseExecutableSymbols(
|
|
172
|
+
repoPath: string,
|
|
173
|
+
filePath: string,
|
|
174
|
+
context?: RepositorySourceContext,
|
|
175
|
+
): Promise<{ symbols: ExecutableSymbolFact[]; calls: SymbolCallFact[] }> {
|
|
176
|
+
const snapshot = context?.get(filePath);
|
|
177
|
+
const text = snapshot?.text
|
|
178
|
+
?? await fs.readFile(path.join(repoPath, filePath), 'utf8');
|
|
179
|
+
const source = snapshot?.sourceFile() ?? ts.createSourceFile(
|
|
180
|
+
filePath, text, ts.ScriptTarget.Latest, true,
|
|
181
|
+
filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS,
|
|
182
|
+
);
|
|
173
183
|
const sourceFile = normalizePath(filePath);
|
|
174
184
|
const symbols: ExecutableSymbolFact[] = [];
|
|
175
185
|
const calls: SymbolCallFact[] = [];
|
|
@@ -1,4 +1,58 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import ts from 'typescript';
|
|
4
|
+
import { normalizePath } from '../utils/path-utils.js';
|
|
5
|
+
|
|
6
|
+
export interface SourceContextInstrumentation {
|
|
7
|
+
onSourceRead?: (
|
|
8
|
+
repoPath: string,
|
|
9
|
+
filePath: string,
|
|
10
|
+
) => void | Promise<void>;
|
|
11
|
+
onAstCreated?: (repoPath: string, filePath: string) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SourceFileSnapshot {
|
|
15
|
+
repoPath: string;
|
|
16
|
+
filePath: string;
|
|
17
|
+
text: string;
|
|
18
|
+
sizeBytes: number;
|
|
19
|
+
sourceFile: () => ts.SourceFile;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RepositorySourceContext {
|
|
23
|
+
get: (filePath: string) => SourceFileSnapshot | undefined;
|
|
24
|
+
entries: () => SourceFileSnapshot[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function loadRepositorySourceContext(
|
|
28
|
+
repoPath: string,
|
|
29
|
+
filePaths: string[],
|
|
30
|
+
instrumentation?: SourceContextInstrumentation,
|
|
31
|
+
): Promise<RepositorySourceContext> {
|
|
32
|
+
const snapshots = new Map<string, SourceFileSnapshot>();
|
|
33
|
+
for (const inputPath of filePaths) {
|
|
34
|
+
const filePath = normalizePath(inputPath);
|
|
35
|
+
await instrumentation?.onSourceRead?.(repoPath, filePath);
|
|
36
|
+
const text = await fs.readFile(path.join(repoPath, filePath), 'utf8');
|
|
37
|
+
let ast: ts.SourceFile | undefined;
|
|
38
|
+
snapshots.set(filePath, {
|
|
39
|
+
repoPath,
|
|
40
|
+
filePath,
|
|
41
|
+
text,
|
|
42
|
+
sizeBytes: Buffer.byteLength(text),
|
|
43
|
+
sourceFile: () => {
|
|
44
|
+
if (ast) return ast;
|
|
45
|
+
instrumentation?.onAstCreated?.(repoPath, filePath);
|
|
46
|
+
ast = createSourceFile(filePath, text);
|
|
47
|
+
return ast;
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
get: (filePath) => snapshots.get(normalizePath(filePath)),
|
|
53
|
+
entries: () => [...snapshots.values()],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
2
56
|
export function createSourceFile(
|
|
3
57
|
filePath: string,
|
|
4
58
|
text: string
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { DynamicMode } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export interface DynamicTemplates {
|
|
4
|
+
servicePath?: string;
|
|
5
|
+
operationPath?: string;
|
|
6
|
+
alias?: string;
|
|
7
|
+
destination?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DynamicVariableProvenance extends Record<string, unknown> {
|
|
11
|
+
sourceKind: string;
|
|
12
|
+
value: string;
|
|
13
|
+
rule: string;
|
|
14
|
+
template?: string;
|
|
15
|
+
matchedName?: string;
|
|
16
|
+
normalizedForm?: string;
|
|
17
|
+
sourceRepo?: string;
|
|
18
|
+
sourceFile?: string;
|
|
19
|
+
sourceLine?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DynamicVariableConflict {
|
|
23
|
+
key: string;
|
|
24
|
+
values: string[];
|
|
25
|
+
reason: string;
|
|
26
|
+
sources: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DynamicTargetCandidate {
|
|
30
|
+
candidateOperationId: number;
|
|
31
|
+
repoId?: number;
|
|
32
|
+
repoName: string;
|
|
33
|
+
packageName?: string;
|
|
34
|
+
serviceName: string;
|
|
35
|
+
qualifiedName: string;
|
|
36
|
+
servicePath: string;
|
|
37
|
+
operationPath: string;
|
|
38
|
+
operationName: string;
|
|
39
|
+
sourceFile: string;
|
|
40
|
+
sourceLine: number;
|
|
41
|
+
originalTemplates: DynamicTemplates;
|
|
42
|
+
effectiveValues: DynamicTemplates;
|
|
43
|
+
requiredVariables: string[];
|
|
44
|
+
requiredVariableSources: Record<string, string[]>;
|
|
45
|
+
suppliedVariables: Record<string, string>;
|
|
46
|
+
completeVariables: Record<string, string>;
|
|
47
|
+
derivedVariables: Record<string, string>;
|
|
48
|
+
derivedVariableSources: Record<string, DynamicVariableProvenance>;
|
|
49
|
+
derivationProvenance: Record<string, DynamicVariableProvenance[]>;
|
|
50
|
+
missingVariables: string[];
|
|
51
|
+
conflicts: DynamicVariableConflict[];
|
|
52
|
+
score: number;
|
|
53
|
+
explicitSignalStrength: number;
|
|
54
|
+
reasons: string[];
|
|
55
|
+
rejectedReasons: string[];
|
|
56
|
+
inferenceBlockReasons: string[];
|
|
57
|
+
viable: boolean;
|
|
58
|
+
rejected: boolean;
|
|
59
|
+
selected: boolean;
|
|
60
|
+
exploratory: boolean;
|
|
61
|
+
cli?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface DynamicTargetAnalysis {
|
|
65
|
+
mode: DynamicMode;
|
|
66
|
+
maxCandidates: number;
|
|
67
|
+
candidateCount: number;
|
|
68
|
+
viableCandidateCount: number;
|
|
69
|
+
rejectedCandidateCount: number;
|
|
70
|
+
shownCandidateCount: number;
|
|
71
|
+
omittedCandidateCount: number;
|
|
72
|
+
shownRejectedCandidateCount: number;
|
|
73
|
+
omittedRejectedCandidateCount: number;
|
|
74
|
+
missingVariables: string[];
|
|
75
|
+
requiredVariables: string[];
|
|
76
|
+
suppliedVariables: Record<string, string>;
|
|
77
|
+
appliedSuppliedVariables: Record<string, string>;
|
|
78
|
+
substitutedSignals: DynamicTemplates;
|
|
79
|
+
candidates: DynamicTargetCandidate[];
|
|
80
|
+
shownCandidates: DynamicTargetCandidate[];
|
|
81
|
+
rejectedCandidates: DynamicTargetCandidate[];
|
|
82
|
+
suggestedVarSets: Array<{ variables: Record<string, string>; cli: string }>;
|
|
83
|
+
inference: Record<string, unknown>;
|
|
84
|
+
}
|