@saptools/service-flow 0.1.55 → 0.1.57
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 +11 -0
- package/README.md +8 -5
- package/TECHNICAL-NOTE.md +6 -0
- package/dist/{chunk-GXYVIHJ5.js → chunk-DCOFNDKS.js} +386 -183
- package/dist/chunk-DCOFNDKS.js.map +1 -0
- package/dist/cli.js +14 -8
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/db/repositories.ts +77 -22
- package/src/parsers/000-direct-query-execution.ts +287 -0
- package/src/parsers/outbound-call-parser.ts +10 -73
- package/src/parsers/symbol-parser.ts +12 -6
- package/dist/chunk-GXYVIHJ5.js.map +0 -1
package/dist/index.js
CHANGED
package/package.json
CHANGED
package/src/db/repositories.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
1
2
|
import type { Db } from './connection.js';
|
|
2
3
|
import { projectBounded } from '../utils/000-bounded-projection.js';
|
|
3
4
|
import type {
|
|
@@ -390,34 +391,88 @@ export function insertSymbolCalls(db: Db, repoId: number, rows: SymbolCallFact[]
|
|
|
390
391
|
for (const r of rows) {
|
|
391
392
|
const caller = callerStmt.get(repoId, r.sourceFile, r.callerQualifiedName) as { id?: number } | undefined;
|
|
392
393
|
const target = resolveSymbolCallTarget(db, repoId, r);
|
|
393
|
-
insertStmt.run(repoId, caller?.id, target.id, r.calleeExpression, r.importSource, r.sourceFile, r.sourceLine, target.status, 0.8, JSON.stringify({ ...r.evidence, candidateStrategy: target.strategy, candidateCount: target.candidateCount }), target.reason);
|
|
394
|
+
insertStmt.run(repoId, caller?.id, target.id, r.calleeExpression, r.importSource, r.sourceFile, r.sourceLine, target.status, 0.8, JSON.stringify({ ...r.evidence, candidateStrategy: target.strategy, candidateCount: target.candidateCount, resolvedModulePath: target.resolvedModulePath }), target.reason);
|
|
394
395
|
}
|
|
395
396
|
}
|
|
397
|
+
interface SymbolTargetRow { id: number; kind?: string; sourceFile?: string | null; evidenceJson?: string | null }
|
|
398
|
+
interface SymbolCallResolution { id: number | null; status: 'resolved' | 'ambiguous' | 'unresolved'; reason: string | null; strategy: string; candidateCount: number; resolvedModulePath?: string }
|
|
399
|
+
const stripExt = (value: string): string => value.replace(/\.(ts|tsx|js|jsx|cds)$/, '');
|
|
400
|
+
function symbolTargetRows(rows: Array<Record<string, unknown>>): SymbolTargetRow[] {
|
|
401
|
+
return rows.flatMap((row) => typeof row.id === 'number' ? [{ id: row.id, kind: typeof row.kind === 'string' ? row.kind : undefined, sourceFile: nullableString(row.sourceFile), evidenceJson: nullableString(row.evidenceJson) }] : []);
|
|
402
|
+
}
|
|
403
|
+
function relativeModuleTargets(callerSourceFile: string, importSource: string): Set<string> {
|
|
404
|
+
const base = posix.dirname(callerSourceFile);
|
|
405
|
+
const joined = stripExt(posix.normalize(posix.join(base, importSource)));
|
|
406
|
+
return new Set([joined, `${joined}/index`]);
|
|
407
|
+
}
|
|
408
|
+
function moduleRows(rows: SymbolTargetRow[], r: SymbolCallFact): SymbolTargetRow[] {
|
|
409
|
+
if (!r.importSource) return [];
|
|
410
|
+
const targets = relativeModuleTargets(r.sourceFile, r.importSource);
|
|
411
|
+
return rows.filter((row) => typeof row.sourceFile === 'string' && targets.has(stripExt(row.sourceFile)));
|
|
412
|
+
}
|
|
413
|
+
function resolvedSymbol(row: SymbolTargetRow, strategy: string, candidateCount: number, moduleScoped = false): SymbolCallResolution {
|
|
414
|
+
return { id: row.id, status: 'resolved', reason: null, strategy, candidateCount, resolvedModulePath: moduleScoped && row.sourceFile ? stripExt(row.sourceFile) : undefined };
|
|
415
|
+
}
|
|
416
|
+
function exportedSymbolRows(db: Db, repoId: number, r: SymbolCallFact): SymbolTargetRow[] {
|
|
417
|
+
return symbolTargetRows(db.prepare('SELECT id,kind,source_file sourceFile,evidence_json evidenceJson FROM symbols WHERE repo_id=? AND source_file<>? AND exported=1 AND (exported_name=? OR name=? OR qualified_name=?) ORDER BY id').all(repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.calleeLocalName));
|
|
418
|
+
}
|
|
396
419
|
function isRelativeImportedSymbolCall(r: SymbolCallFact): boolean {
|
|
397
420
|
return Boolean(r.importSource?.startsWith('.'));
|
|
398
421
|
}
|
|
399
|
-
function
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if (
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
422
|
+
function sameFileResolution(db: Db, repoId: number, r: SymbolCallFact, relation: unknown): SymbolCallResolution | undefined {
|
|
423
|
+
const bareImport = relation === 'relative_import' && isRelativeImportedSymbolCall(r) && !String(r.calleeLocalName).includes('.');
|
|
424
|
+
if (bareImport || relation === 'relative_import_namespace_member' || relation === 'package_import') return undefined;
|
|
425
|
+
const rows = symbolTargetRows(db.prepare('SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND (name=? OR qualified_name=?) ORDER BY id').all(repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName));
|
|
426
|
+
if (rows.length === 1 && rows[0]) return resolvedSymbol(rows[0], 'same_file_exact', 1);
|
|
427
|
+
return rows.length > 1 ? { id: null, status: 'ambiguous', reason: 'Multiple same-file symbol targets matched exactly', strategy: 'same_file_exact', candidateCount: rows.length } : undefined;
|
|
428
|
+
}
|
|
429
|
+
function classInstanceResolution(db: Db, repoId: number, r: SymbolCallFact, relation: unknown): SymbolCallResolution | undefined {
|
|
430
|
+
if (relation !== 'class_instance_method' || !isRelativeImportedSymbolCall(r)) return undefined;
|
|
431
|
+
const rows = symbolTargetRows(db.prepare('SELECT id FROM symbols WHERE repo_id=? AND source_file<>? AND qualified_name=? ORDER BY id').all(repoId, r.sourceFile, r.calleeLocalName));
|
|
432
|
+
if (rows.length === 1 && rows[0]) return resolvedSymbol(rows[0], 'relative_import_class_instance_method', 1);
|
|
433
|
+
return rows.length > 1 ? { id: null, status: 'ambiguous', reason: 'Multiple relative class instance method targets matched exactly', strategy: 'relative_import_class_instance_method', candidateCount: rows.length } : undefined;
|
|
434
|
+
}
|
|
435
|
+
function namespaceResolution(db: Db, repoId: number, r: SymbolCallFact, relation: unknown): SymbolCallResolution | undefined {
|
|
436
|
+
if (relation !== 'relative_import_namespace_member' || !isRelativeImportedSymbolCall(r)) return undefined;
|
|
437
|
+
const rows = moduleRows(exportedSymbolRows(db, repoId, r), r);
|
|
438
|
+
if (rows.length === 1 && rows[0]) return resolvedSymbol(rows[0], 'relative_import_namespace_member', 1, true);
|
|
439
|
+
if (rows.length > 1) return { id: null, status: 'ambiguous', reason: 'Multiple namespace member targets matched the imported module', strategy: 'relative_import_namespace_member', candidateCount: rows.length };
|
|
440
|
+
return { id: null, status: 'unresolved', reason: 'No namespace member target matched the imported module', strategy: 'relative_import_namespace_member', candidateCount: 0 };
|
|
441
|
+
}
|
|
442
|
+
function proxyResolution(rows: SymbolTargetRow[], r: SymbolCallFact, relation: unknown): SymbolCallResolution | undefined {
|
|
443
|
+
if (relation !== 'relative_import_proxy_member' || rows.length <= 1) return undefined;
|
|
444
|
+
const mapped = rows.filter((row) => String(row.evidenceJson ?? '').includes('exported_object_shorthand') || String(row.evidenceJson ?? '').includes('exported_object_literal'));
|
|
445
|
+
if (mapped.length > 0) {
|
|
446
|
+
const concrete = rows.find((row) => row.kind !== 'object_alias') ?? mapped[0];
|
|
447
|
+
return { id: concrete?.id ?? null, status: 'resolved', reason: null, strategy: 'proxy_member_exported_object_map', candidateCount: rows.length };
|
|
417
448
|
}
|
|
418
|
-
|
|
419
|
-
if (
|
|
420
|
-
return { id: null, status: '
|
|
449
|
+
const scoped = moduleRows(rows, r);
|
|
450
|
+
if (scoped.length === 1 && scoped[0]) return resolvedSymbol(scoped[0], 'relative_import_path_disambiguated', rows.length, true);
|
|
451
|
+
return { id: null, status: 'ambiguous', reason: 'Proxy member target requires explicit factory/module/type evidence; global member name is ambiguous', strategy: 'proxy_member_no_global_name_fallback', candidateCount: rows.length };
|
|
452
|
+
}
|
|
453
|
+
function exportedResolution(rows: SymbolTargetRow[], r: SymbolCallFact, relation: unknown): SymbolCallResolution | undefined {
|
|
454
|
+
if (rows.length === 1 && rows[0]) return resolvedSymbol(rows[0], relation === 'relative_import_proxy_member' ? 'proxy_member_unique_exported_candidate' : 'relative_import_exported_exact', 1, moduleRows(rows, r).length === 1);
|
|
455
|
+
if (rows.length <= 1) return undefined;
|
|
456
|
+
const scoped = isRelativeImportedSymbolCall(r) ? moduleRows(rows, r) : [];
|
|
457
|
+
if (scoped.length === 1 && scoped[0]) return resolvedSymbol(scoped[0], 'relative_import_path_disambiguated', rows.length, true);
|
|
458
|
+
return { id: null, status: 'ambiguous', reason: 'Multiple exported symbol targets matched exactly', strategy: 'exported_exact', candidateCount: rows.length };
|
|
459
|
+
}
|
|
460
|
+
function accessorResolution(db: Db, repoId: number, r: SymbolCallFact, relation: unknown): SymbolCallResolution | undefined {
|
|
461
|
+
if (relation !== 'relative_import' || !isRelativeImportedSymbolCall(r) || !/^[^.]+\.[^.]+$/.test(String(r.calleeLocalName))) return undefined;
|
|
462
|
+
const methodRows = symbolTargetRows(db.prepare("SELECT id,kind,source_file sourceFile FROM symbols WHERE repo_id=? AND source_file<>? AND kind='method' AND qualified_name=? ORDER BY id").all(repoId, r.sourceFile, r.calleeLocalName));
|
|
463
|
+
const scoped = moduleRows(methodRows, r);
|
|
464
|
+
if (scoped.length === 1 && scoped[0]) return resolvedSymbol(scoped[0], 'relative_import_static_accessor_instance_method', 1, true);
|
|
465
|
+
return scoped.length > 1 ? { id: null, status: 'ambiguous', reason: 'Multiple static-accessor instance method targets matched the imported module', strategy: 'relative_import_static_accessor_instance_method', candidateCount: scoped.length } : undefined;
|
|
466
|
+
}
|
|
467
|
+
function resolveSymbolCallTarget(db: Db, repoId: number, r: SymbolCallFact): SymbolCallResolution {
|
|
468
|
+
const relation = r.evidence.relation;
|
|
469
|
+
const early = sameFileResolution(db, repoId, r, relation) ?? classInstanceResolution(db, repoId, r, relation) ?? namespaceResolution(db, repoId, r, relation);
|
|
470
|
+
if (early) return early;
|
|
471
|
+
const rows = relation === 'package_import' ? [] : exportedSymbolRows(db, repoId, r);
|
|
472
|
+
const matched = proxyResolution(rows, r, relation) ?? exportedResolution(rows, r, relation) ?? accessorResolution(db, repoId, r, relation);
|
|
473
|
+
if (matched) return matched;
|
|
474
|
+
if (relation === 'package_import') return { id: null, status: 'unresolved', reason: 'Package import target resolution requires a post-publication workspace pass', strategy: 'package_import_unresolved', candidateCount: 0 };
|
|
475
|
+
return { id: null, status: 'unresolved', reason: 'No local symbol target matched exactly', strategy: relation === 'relative_import_proxy_member' ? 'proxy_member_no_global_name_fallback' : 'exact_symbol_match', candidateCount: 0 };
|
|
421
476
|
}
|
|
422
477
|
export function insertCalls(
|
|
423
478
|
db: Db,
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
const capQueryBuilderRoots = new Set([
|
|
4
|
+
'SELECT.from',
|
|
5
|
+
'SELECT.one.from',
|
|
6
|
+
'SELECT.one',
|
|
7
|
+
'INSERT.into',
|
|
8
|
+
'UPSERT.into',
|
|
9
|
+
'UPDATE.entity',
|
|
10
|
+
'UPDATE',
|
|
11
|
+
'DELETE.from',
|
|
12
|
+
]);
|
|
13
|
+
const promiseValueShadowCache = new WeakMap<ts.SourceFile, boolean>();
|
|
14
|
+
|
|
15
|
+
export type DirectQueryExecutionContext =
|
|
16
|
+
| 'await'
|
|
17
|
+
| 'async_return'
|
|
18
|
+
| 'promise_return'
|
|
19
|
+
| 'promise_aggregate';
|
|
20
|
+
|
|
21
|
+
export interface DirectQueryBuilderStatement {
|
|
22
|
+
root: ts.CallExpression;
|
|
23
|
+
logicalCall: ts.CallExpression;
|
|
24
|
+
statement: ts.Expression;
|
|
25
|
+
executionContext: DirectQueryExecutionContext;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isCapQueryBuilderRootName(name: string): boolean {
|
|
29
|
+
return capQueryBuilderRoots.has(name);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function queryBuilderRoot(
|
|
33
|
+
expression: ts.Expression,
|
|
34
|
+
): ts.CallExpression | undefined {
|
|
35
|
+
const unwrapped = unwrapQueryExpression(expression);
|
|
36
|
+
if (!ts.isCallExpression(unwrapped)) return undefined;
|
|
37
|
+
if (isCapQueryBuilderRootName(expressionName(unwrapped.expression)))
|
|
38
|
+
return unwrapped;
|
|
39
|
+
return ts.isPropertyAccessExpression(unwrapped.expression)
|
|
40
|
+
? queryBuilderRoot(unwrapped.expression.expression)
|
|
41
|
+
: undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function directQueryBuilderStatement(
|
|
45
|
+
node: ts.CallExpression,
|
|
46
|
+
): DirectQueryBuilderStatement | undefined {
|
|
47
|
+
const root = queryBuilderRoot(node);
|
|
48
|
+
if (!root) return undefined;
|
|
49
|
+
const logicalCall = outerFluentQueryCall(root);
|
|
50
|
+
if (logicalCall !== node) return undefined;
|
|
51
|
+
const expression = outerTransparentExpression(logicalCall);
|
|
52
|
+
const awaitExpression = directAwaitExpression(expression);
|
|
53
|
+
if (awaitExpression)
|
|
54
|
+
return { root, logicalCall, statement: awaitExpression, executionContext: 'await' };
|
|
55
|
+
const returnContext = returnExecutionContext(expression);
|
|
56
|
+
if (returnContext)
|
|
57
|
+
return { root, logicalCall, statement: expression, executionContext: returnContext };
|
|
58
|
+
if (isAwaitedPromiseAllElement(expression))
|
|
59
|
+
return { root, logicalCall, statement: expression, executionContext: 'promise_aggregate' };
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function expressionName(expression: ts.Expression): string {
|
|
64
|
+
if (ts.isIdentifier(expression)) return expression.text;
|
|
65
|
+
if (ts.isPropertyAccessExpression(expression))
|
|
66
|
+
return `${expressionName(expression.expression)}.${expression.name.text}`;
|
|
67
|
+
return expression.getText();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function unwrapQueryExpression(expression: ts.Expression): ts.Expression {
|
|
71
|
+
if (ts.isParenthesizedExpression(expression) || ts.isAwaitExpression(expression)
|
|
72
|
+
|| ts.isAsExpression(expression) || ts.isTypeAssertionExpression(expression)
|
|
73
|
+
|| ts.isNonNullExpression(expression) || ts.isSatisfiesExpression(expression))
|
|
74
|
+
return unwrapQueryExpression(expression.expression);
|
|
75
|
+
return expression;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function wrapperParent(node: ts.Expression): ts.Expression | undefined {
|
|
79
|
+
const parent = node.parent;
|
|
80
|
+
if ((ts.isParenthesizedExpression(parent) || ts.isAsExpression(parent)
|
|
81
|
+
|| ts.isTypeAssertionExpression(parent) || ts.isNonNullExpression(parent)
|
|
82
|
+
|| ts.isSatisfiesExpression(parent)) && parent.expression === node)
|
|
83
|
+
return parent;
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fluentCallParent(node: ts.Expression): ts.CallExpression | undefined {
|
|
88
|
+
const property = node.parent;
|
|
89
|
+
if (!ts.isPropertyAccessExpression(property) || property.expression !== node)
|
|
90
|
+
return undefined;
|
|
91
|
+
const call = property.parent;
|
|
92
|
+
return ts.isCallExpression(call) && call.expression === property ? call : undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function outerFluentQueryCall(root: ts.CallExpression): ts.CallExpression {
|
|
96
|
+
let current: ts.Expression = root;
|
|
97
|
+
let outer = root;
|
|
98
|
+
while (true) {
|
|
99
|
+
const wrapper = wrapperParent(current);
|
|
100
|
+
if (wrapper) {
|
|
101
|
+
current = wrapper;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const next = fluentCallParent(current);
|
|
105
|
+
if (!next) return outer;
|
|
106
|
+
outer = next;
|
|
107
|
+
current = next;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function outerTransparentExpression(expression: ts.Expression): ts.Expression {
|
|
112
|
+
let current = expression;
|
|
113
|
+
while (true) {
|
|
114
|
+
const wrapper = wrapperParent(current);
|
|
115
|
+
if (!wrapper) return current;
|
|
116
|
+
current = wrapper;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function directAwaitExpression(
|
|
121
|
+
expression: ts.Expression,
|
|
122
|
+
): ts.AwaitExpression | undefined {
|
|
123
|
+
const parent = expression.parent;
|
|
124
|
+
return ts.isAwaitExpression(parent) && parent.expression === expression
|
|
125
|
+
? parent
|
|
126
|
+
: undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function returnExecutionContext(
|
|
130
|
+
expression: ts.Expression,
|
|
131
|
+
): DirectQueryExecutionContext | undefined {
|
|
132
|
+
const callable = returnedExpressionCallable(expression);
|
|
133
|
+
if (!callable) return undefined;
|
|
134
|
+
if (hasAsyncModifier(callable)) return 'async_return';
|
|
135
|
+
return hasGuaranteedPromiseReturn(callable) ? 'promise_return' : undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function returnedExpressionCallable(
|
|
139
|
+
expression: ts.Expression,
|
|
140
|
+
): ts.FunctionLikeDeclaration | undefined {
|
|
141
|
+
const parent = expression.parent;
|
|
142
|
+
if (ts.isArrowFunction(parent) && parent.body === expression) return parent;
|
|
143
|
+
if (!ts.isReturnStatement(parent) || parent.expression !== expression)
|
|
144
|
+
return undefined;
|
|
145
|
+
return nearestCallable(parent);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function nearestCallable(node: ts.Node): ts.FunctionLikeDeclaration | undefined {
|
|
149
|
+
let current = node.parent;
|
|
150
|
+
while (current) {
|
|
151
|
+
if (isRuntimeCallable(current)) return current;
|
|
152
|
+
current = current.parent;
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isRuntimeCallable(node: ts.Node): node is ts.FunctionLikeDeclaration {
|
|
158
|
+
return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)
|
|
159
|
+
|| ts.isArrowFunction(node) || ts.isMethodDeclaration(node)
|
|
160
|
+
|| ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node)
|
|
161
|
+
|| ts.isSetAccessorDeclaration(node);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function hasAsyncModifier(node: ts.Node): boolean {
|
|
165
|
+
return !isGeneratorCallable(node) && ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some(
|
|
166
|
+
(modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword,
|
|
167
|
+
) ?? false);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isGeneratorCallable(node: ts.Node): boolean {
|
|
171
|
+
return (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)
|
|
172
|
+
|| ts.isMethodDeclaration(node)) && Boolean(node.asteriskToken);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function hasGuaranteedPromiseReturn(
|
|
176
|
+
callable: ts.FunctionLikeDeclaration,
|
|
177
|
+
): boolean {
|
|
178
|
+
const returnType = declaredReturnType(callable);
|
|
179
|
+
return Boolean(returnType && isGuaranteedPromiseType(returnType));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function declaredReturnType(
|
|
183
|
+
callable: ts.FunctionLikeDeclaration,
|
|
184
|
+
): ts.TypeNode | undefined {
|
|
185
|
+
if (ts.isFunctionDeclaration(callable) || ts.isFunctionExpression(callable)
|
|
186
|
+
|| ts.isArrowFunction(callable) || ts.isMethodDeclaration(callable))
|
|
187
|
+
return callable.type;
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isGuaranteedPromiseType(type: ts.TypeNode): boolean {
|
|
192
|
+
if (ts.isParenthesizedTypeNode(type))
|
|
193
|
+
return isGuaranteedPromiseType(type.type);
|
|
194
|
+
if (ts.isTypeReferenceNode(type))
|
|
195
|
+
return isStandardPromiseTypeName(type.typeName);
|
|
196
|
+
if (ts.isUnionTypeNode(type))
|
|
197
|
+
return type.types.length > 0 && type.types.every(isGuaranteedPromiseType);
|
|
198
|
+
if (ts.isIntersectionTypeNode(type))
|
|
199
|
+
return type.types.some(isGuaranteedPromiseType);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isStandardPromiseTypeName(name: ts.EntityName): boolean {
|
|
204
|
+
if (ts.isIdentifier(name))
|
|
205
|
+
return name.text === 'Promise' || name.text === 'PromiseLike';
|
|
206
|
+
return ts.isIdentifier(name.left)
|
|
207
|
+
&& (name.left.text === 'globalThis' || name.left.text === 'global')
|
|
208
|
+
&& (name.right.text === 'Promise' || name.right.text === 'PromiseLike');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isAwaitedPromiseAllElement(expression: ts.Expression): boolean {
|
|
212
|
+
const array = directArrayParent(expression);
|
|
213
|
+
if (!array) return false;
|
|
214
|
+
const aggregate = aggregateCallForArray(array);
|
|
215
|
+
return Boolean(aggregate && isBuiltInPromiseAll(aggregate)
|
|
216
|
+
&& directAwaitExpression(outerTransparentExpression(aggregate)));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function directArrayParent(
|
|
220
|
+
expression: ts.Expression,
|
|
221
|
+
): ts.ArrayLiteralExpression | undefined {
|
|
222
|
+
const parent = expression.parent;
|
|
223
|
+
return ts.isArrayLiteralExpression(parent) && parent.elements.some(
|
|
224
|
+
(element) => element === expression,
|
|
225
|
+
) ? parent : undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function aggregateCallForArray(
|
|
229
|
+
array: ts.ArrayLiteralExpression,
|
|
230
|
+
): ts.CallExpression | undefined {
|
|
231
|
+
const argument = outerTransparentExpression(array);
|
|
232
|
+
const parent = argument.parent;
|
|
233
|
+
return ts.isCallExpression(parent) && parent.arguments.length === 1
|
|
234
|
+
&& parent.arguments[0] === argument ? parent : undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function isBuiltInPromiseAll(call: ts.CallExpression): boolean {
|
|
238
|
+
return ts.isPropertyAccessExpression(call.expression)
|
|
239
|
+
&& ts.isIdentifier(call.expression.expression)
|
|
240
|
+
&& call.expression.expression.text === 'Promise'
|
|
241
|
+
&& call.expression.name.text === 'all'
|
|
242
|
+
&& !hasPromiseValueShadow(call.getSourceFile());
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function hasPromiseValueShadow(source: ts.SourceFile): boolean {
|
|
246
|
+
const cached = promiseValueShadowCache.get(source);
|
|
247
|
+
if (cached !== undefined) return cached;
|
|
248
|
+
let shadowed = false;
|
|
249
|
+
const visit = (node: ts.Node): void => {
|
|
250
|
+
if (shadowed) return;
|
|
251
|
+
if (declaresPromiseValue(node)) {
|
|
252
|
+
shadowed = true;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
ts.forEachChild(node, visit);
|
|
256
|
+
};
|
|
257
|
+
visit(source);
|
|
258
|
+
promiseValueShadowCache.set(source, shadowed);
|
|
259
|
+
return shadowed;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function declaresPromiseValue(node: ts.Node): boolean {
|
|
263
|
+
if (ts.isVariableDeclaration(node) || ts.isParameter(node))
|
|
264
|
+
return bindingNameIsPromise(node.name);
|
|
265
|
+
if (ts.isImportClause(node))
|
|
266
|
+
return !node.isTypeOnly && nodeIsPromise(node.name);
|
|
267
|
+
if (ts.isImportSpecifier(node))
|
|
268
|
+
return !node.isTypeOnly && nodeIsPromise(node.name);
|
|
269
|
+
if (ts.isNamespaceImport(node) || ts.isImportEqualsDeclaration(node))
|
|
270
|
+
return nodeIsPromise(node.name);
|
|
271
|
+
if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)
|
|
272
|
+
|| ts.isEnumDeclaration(node) || ts.isModuleDeclaration(node))
|
|
273
|
+
return nodeIsPromise(node.name);
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function bindingNameIsPromise(name: ts.BindingName): boolean {
|
|
278
|
+
if (ts.isIdentifier(name)) return name.text === 'Promise';
|
|
279
|
+
if (ts.isObjectBindingPattern(name))
|
|
280
|
+
return name.elements.some((element) => bindingNameIsPromise(element.name));
|
|
281
|
+
return name.elements.some((element) => ts.isBindingElement(element)
|
|
282
|
+
&& bindingNameIsPromise(element.name));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function nodeIsPromise(name: ts.Node | undefined): boolean {
|
|
286
|
+
return Boolean(name && ts.isIdentifier(name) && name.text === 'Promise');
|
|
287
|
+
}
|
|
@@ -8,6 +8,12 @@ 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 {
|
|
12
|
+
directQueryBuilderStatement,
|
|
13
|
+
isCapQueryBuilderRootName,
|
|
14
|
+
queryBuilderRoot,
|
|
15
|
+
type DirectQueryBuilderStatement,
|
|
16
|
+
} from './000-direct-query-execution.js';
|
|
11
17
|
import type { RepositorySourceContext } from './ts-project.js';
|
|
12
18
|
import {
|
|
13
19
|
analyzeOperationPath,
|
|
@@ -53,82 +59,12 @@ function queryEntityFromAst(expr: ts.Expression, initializers = new Map<string,
|
|
|
53
59
|
if (ts.isCallExpression(unwrapped)) {
|
|
54
60
|
const name = expressionName(unwrapped.expression);
|
|
55
61
|
if (name === 'cds.run') return queryEntityFromAst(unwrapped.arguments[0], initializers);
|
|
56
|
-
if (
|
|
62
|
+
if (isCapQueryBuilderRootName(name)) return entityFromExpression(unwrapped.arguments[0]);
|
|
57
63
|
const receiver = ts.isPropertyAccessExpression(unwrapped.expression) ? unwrapped.expression.expression : undefined;
|
|
58
64
|
if (receiver) return queryEntityFromAst(receiver, initializers);
|
|
59
65
|
}
|
|
60
66
|
return undefined;
|
|
61
67
|
}
|
|
62
|
-
const capQueryBuilderRoots = new Set([
|
|
63
|
-
'SELECT.from',
|
|
64
|
-
'SELECT.one.from',
|
|
65
|
-
'SELECT.one',
|
|
66
|
-
'INSERT.into',
|
|
67
|
-
'UPSERT.into',
|
|
68
|
-
'UPDATE.entity',
|
|
69
|
-
'UPDATE',
|
|
70
|
-
'DELETE.from',
|
|
71
|
-
]);
|
|
72
|
-
interface DirectQueryBuilderStatement {
|
|
73
|
-
root: ts.CallExpression;
|
|
74
|
-
logicalCall: ts.CallExpression;
|
|
75
|
-
awaitExpression: ts.AwaitExpression;
|
|
76
|
-
}
|
|
77
|
-
function wrapperParent(node: ts.Expression): ts.Expression | undefined {
|
|
78
|
-
const parent = node.parent;
|
|
79
|
-
if ((ts.isParenthesizedExpression(parent) || ts.isAsExpression(parent)
|
|
80
|
-
|| ts.isTypeAssertionExpression(parent) || ts.isNonNullExpression(parent)
|
|
81
|
-
|| ts.isSatisfiesExpression(parent)) && parent.expression === node)
|
|
82
|
-
return parent;
|
|
83
|
-
return undefined;
|
|
84
|
-
}
|
|
85
|
-
function fluentCallParent(node: ts.Expression): ts.CallExpression | undefined {
|
|
86
|
-
const property = node.parent;
|
|
87
|
-
if (!ts.isPropertyAccessExpression(property) || property.expression !== node) return undefined;
|
|
88
|
-
const call = property.parent;
|
|
89
|
-
return ts.isCallExpression(call) && call.expression === property ? call : undefined;
|
|
90
|
-
}
|
|
91
|
-
function queryBuilderRoot(expr: ts.Expression): ts.CallExpression | undefined {
|
|
92
|
-
const unwrapped = unwrapQueryExpression(expr);
|
|
93
|
-
if (!ts.isCallExpression(unwrapped)) return undefined;
|
|
94
|
-
if (capQueryBuilderRoots.has(expressionName(unwrapped.expression))) return unwrapped;
|
|
95
|
-
return ts.isPropertyAccessExpression(unwrapped.expression)
|
|
96
|
-
? queryBuilderRoot(unwrapped.expression.expression)
|
|
97
|
-
: undefined;
|
|
98
|
-
}
|
|
99
|
-
function outerFluentQueryCall(root: ts.CallExpression): ts.CallExpression {
|
|
100
|
-
let current: ts.Expression = root;
|
|
101
|
-
let outer = root;
|
|
102
|
-
while (true) {
|
|
103
|
-
const wrapper = wrapperParent(current);
|
|
104
|
-
if (wrapper) {
|
|
105
|
-
current = wrapper;
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
const next = fluentCallParent(current);
|
|
109
|
-
if (!next) return outer;
|
|
110
|
-
outer = next;
|
|
111
|
-
current = next;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
function directQueryBuilderStatement(node: ts.CallExpression): DirectQueryBuilderStatement | undefined {
|
|
115
|
-
const root = queryBuilderRoot(node);
|
|
116
|
-
if (!root) return undefined;
|
|
117
|
-
const logicalCall = outerFluentQueryCall(root);
|
|
118
|
-
if (logicalCall !== node) return undefined;
|
|
119
|
-
let current: ts.Expression = logicalCall;
|
|
120
|
-
while (true) {
|
|
121
|
-
const wrapper = wrapperParent(current);
|
|
122
|
-
if (wrapper) {
|
|
123
|
-
current = wrapper;
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
const parent = current.parent;
|
|
127
|
-
return ts.isAwaitExpression(parent) && parent.expression === current
|
|
128
|
-
? { root, logicalCall, awaitExpression: parent }
|
|
129
|
-
: undefined;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
68
|
function queryBuilderEvidence(source: ts.SourceFile, statement: DirectQueryBuilderStatement): Record<string, unknown> {
|
|
133
69
|
return {
|
|
134
70
|
classifier: 'cap_query_builder_direct',
|
|
@@ -136,8 +72,9 @@ function queryBuilderEvidence(source: ts.SourceFile, statement: DirectQueryBuild
|
|
|
136
72
|
queryRoot: expressionName(statement.root.expression),
|
|
137
73
|
queryRootStartOffset: statement.root.getStart(source),
|
|
138
74
|
queryRootEndOffset: statement.root.getEnd(),
|
|
139
|
-
queryStatementStartOffset: statement.
|
|
140
|
-
queryStatementEndOffset: statement.
|
|
75
|
+
queryStatementStartOffset: statement.statement.getStart(source),
|
|
76
|
+
queryStatementEndOffset: statement.statement.getEnd(),
|
|
77
|
+
queryExecutionContext: statement.executionContext,
|
|
141
78
|
};
|
|
142
79
|
}
|
|
143
80
|
function queryRunEvidence(
|
|
@@ -184,6 +184,7 @@ export async function parseExecutableSymbols(
|
|
|
184
184
|
const symbols: ExecutableSymbolFact[] = [];
|
|
185
185
|
const calls: SymbolCallFact[] = [];
|
|
186
186
|
const imports = new Map<string, string>();
|
|
187
|
+
const namespaceImports = new Set<string>();
|
|
187
188
|
const exportNames = exportDeclarations(source);
|
|
188
189
|
const objectExports = new Set<string>();
|
|
189
190
|
const exportedClasses = new Set<string>();
|
|
@@ -214,7 +215,10 @@ export async function parseExecutableSymbols(
|
|
|
214
215
|
if (clause?.name) imports.set(clause.name.text, sourceText);
|
|
215
216
|
const named = clause?.namedBindings;
|
|
216
217
|
if (named && ts.isNamedImports(named)) for (const el of named.elements) imports.set(el.name.text, sourceText);
|
|
217
|
-
if (named && ts.isNamespaceImport(named))
|
|
218
|
+
if (named && ts.isNamespaceImport(named)) {
|
|
219
|
+
imports.set(named.name.text, sourceText);
|
|
220
|
+
namespaceImports.add(named.name.text);
|
|
221
|
+
}
|
|
218
222
|
}
|
|
219
223
|
if (ts.isVariableStatement(node)) {
|
|
220
224
|
for (const declaration of node.declarationList.declarations) {
|
|
@@ -330,7 +334,9 @@ export async function parseExecutableSymbols(
|
|
|
330
334
|
const instance = (callee.local ? classInstances.get(callee.local) : undefined) ?? (callee.receiver ? classInstances.get(callee.receiver) : undefined);
|
|
331
335
|
const importSource = instance?.importSource ?? proxy?.importSource ?? (callee.local ? imports.get(callee.local) : undefined) ?? (callee.member && callee.local ? imports.get(callee.local) : undefined);
|
|
332
336
|
const directThisMethod = callee.receiver === 'this';
|
|
333
|
-
const
|
|
337
|
+
const namespaceMember = Boolean(callee.member && callee.local && namespaceImports.has(callee.local));
|
|
338
|
+
const packageImport = Boolean(importSource && !isRelativeImport(importSource));
|
|
339
|
+
const targetName = instance && callee.member ? `${instance.className}.${callee.member}` : proxy && callee.member ? callee.member : directThisMethod ? callee.member : namespaceMember ? callee.member : packageImport ? (callee.member ?? callee.local) : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
|
|
334
340
|
const className = caller.qualifiedName.includes('.') ? caller.qualifiedName.split('.')[0] : undefined;
|
|
335
341
|
const thisTarget = directThisMethod && className && callee.member ? `${className}.${callee.member}` : undefined;
|
|
336
342
|
const loggerLike = callee.receiver?.endsWith('.logger') || callee.local === 'logger' || (callee.expression.startsWith('this.logger.') && callee.member ? loggerMembers.has(callee.member) : false);
|
|
@@ -339,11 +345,11 @@ export async function parseExecutableSymbols(
|
|
|
339
345
|
const provenThisMethod = Boolean(thisTarget && localCallables.has(thisTarget));
|
|
340
346
|
const provenRelativeImport = Boolean(isRelativeImport(importSource) && targetName);
|
|
341
347
|
const provenClassInstance = Boolean(instance && callee.member && targetName);
|
|
342
|
-
const
|
|
343
|
-
const ignored = loggerLike || terminalMember ||
|
|
348
|
+
const provenPackageImport = Boolean(packageImport && targetName);
|
|
349
|
+
const ignored = loggerLike || terminalMember || ignoredFrameworkCall(callee);
|
|
344
350
|
const resolvedTarget = provenThisMethod ? thisTarget : targetName;
|
|
345
|
-
const keep = Boolean(resolvedTarget) && !ignored && (provenLocal || provenThisMethod || provenRelativeImport || provenClassInstance);
|
|
346
|
-
if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: resolvedTarget, receiverLocalName: callee.member ? (callee.local ?? callee.receiver) : undefined, importSource, sourceFile, sourceLine: line, evidence: { relation: instance ? 'class_instance_method' : proxy ? 'relative_import_proxy_member' : importSource ? 'relative_import' : provenThisMethod ? 'indexed_this_method' : 'indexed_local_symbol', caller: caller.qualifiedName, targetName: resolvedTarget, instanceVariable: instance ? (instance.propertyName ?? callee.local) : undefined, className: instance?.className, methodName: instance ? callee.member : undefined, classImportSource: instance?.importSource, callArguments: argumentEvidence(node.arguments, source), proxyVariableName: proxy?.variableName, factory: proxy?.factory, factoryExpression: proxy?.factory, factoryImportSource: proxy?.importSource, candidateStrategy: instance ? (instance.importSource ? 'relative_import_class_instance_method' : 'same_file_class_instance_method') : proxy ? 'proxy_member_exact_export_or_unique_member' : undefined } });
|
|
351
|
+
const keep = Boolean(resolvedTarget) && !ignored && (provenLocal || provenThisMethod || provenRelativeImport || provenClassInstance || provenPackageImport);
|
|
352
|
+
if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: resolvedTarget, receiverLocalName: callee.member ? (callee.local ?? callee.receiver) : undefined, importSource, sourceFile, sourceLine: line, evidence: { relation: instance ? 'class_instance_method' : proxy ? 'relative_import_proxy_member' : packageImport ? 'package_import' : namespaceMember ? 'relative_import_namespace_member' : importSource ? 'relative_import' : provenThisMethod ? 'indexed_this_method' : 'indexed_local_symbol', caller: caller.qualifiedName, targetName: resolvedTarget, instanceVariable: instance ? (instance.propertyName ?? callee.local) : undefined, className: instance?.className, methodName: instance ? callee.member : undefined, classImportSource: instance?.importSource, callArguments: argumentEvidence(node.arguments, source), proxyVariableName: proxy?.variableName, factory: proxy?.factory, factoryExpression: proxy?.factory, factoryImportSource: proxy?.importSource, candidateStrategy: instance ? (instance.importSource ? 'relative_import_class_instance_method' : 'same_file_class_instance_method') : proxy ? 'proxy_member_exact_export_or_unique_member' : undefined } });
|
|
347
353
|
}
|
|
348
354
|
}
|
|
349
355
|
ts.forEachChild(node, visitCalls);
|