@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,210 @@
|
|
|
1
|
+
import type { Db } from './connection.js';
|
|
2
|
+
import { ANALYZER_VERSION } from '../version.js';
|
|
3
|
+
import {
|
|
4
|
+
resolveRelativeSymbolCall,
|
|
5
|
+
} from './006-relative-symbol-resolution.js';
|
|
6
|
+
import type { SymbolCallFact } from '../types.js';
|
|
7
|
+
import { parseRelativeImportReference } from
|
|
8
|
+
'../parsers/012-package-fact-contract.js';
|
|
9
|
+
import type { SymbolImportReference } from
|
|
10
|
+
'../parsers/002-symbol-import-bindings.js';
|
|
11
|
+
|
|
12
|
+
export interface RelativeFactCategoryCount {
|
|
13
|
+
category: string;
|
|
14
|
+
count: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface PersistedRelativeCall {
|
|
18
|
+
repoId: number;
|
|
19
|
+
calleeId: number | null;
|
|
20
|
+
status: string;
|
|
21
|
+
reason: string | null;
|
|
22
|
+
fact: SymbolCallFact;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PersistedRelativeRows {
|
|
26
|
+
calls: PersistedRelativeCall[];
|
|
27
|
+
malformedCount: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
31
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
32
|
+
? value as Record<string, unknown> : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseRecord(value: unknown): Record<string, unknown> | undefined {
|
|
36
|
+
if (typeof value !== 'string') return undefined;
|
|
37
|
+
try {
|
|
38
|
+
return record(JSON.parse(value) as unknown);
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function nullableNumber(value: unknown): number | null {
|
|
45
|
+
return typeof value === 'number' ? value : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function all(values: readonly boolean[]): boolean {
|
|
49
|
+
return values.every(Boolean);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function memberReference(binding: SymbolImportReference): boolean {
|
|
53
|
+
return binding.referenceShape !== 'identifier'
|
|
54
|
+
&& typeof binding.referencedMemberName === 'string'
|
|
55
|
+
&& binding.referencedMemberName.length > 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function directRelativeProvenance(
|
|
59
|
+
evidence: Record<string, unknown>,
|
|
60
|
+
binding: SymbolImportReference,
|
|
61
|
+
targetName: string,
|
|
62
|
+
): boolean {
|
|
63
|
+
if (evidence.relation !== 'relative_import') return false;
|
|
64
|
+
return binding.referenceShape !== 'namespace_member'
|
|
65
|
+
&& targetName === binding.requestedPublicName;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function namespaceRelativeProvenance(
|
|
69
|
+
evidence: Record<string, unknown>,
|
|
70
|
+
binding: SymbolImportReference,
|
|
71
|
+
targetName: string,
|
|
72
|
+
): boolean {
|
|
73
|
+
if (evidence.relation !== 'relative_import_namespace_member') return false;
|
|
74
|
+
return binding.referenceShape === 'namespace_member'
|
|
75
|
+
&& targetName === binding.requestedPublicName;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function derivedRelativeProvenance(
|
|
79
|
+
evidence: Record<string, unknown>,
|
|
80
|
+
binding: SymbolImportReference,
|
|
81
|
+
targetName: string,
|
|
82
|
+
): boolean {
|
|
83
|
+
if (!memberReference(binding)) return false;
|
|
84
|
+
if (evidence.relation === 'class_instance_method') {
|
|
85
|
+
if (evidence.methodName !== binding.referencedMemberName) return false;
|
|
86
|
+
return binding.bindingKind === 'esm_default'
|
|
87
|
+
? targetName === `${binding.localName}.${binding.referencedMemberName}`
|
|
88
|
+
: targetName === binding.requestedPublicName;
|
|
89
|
+
}
|
|
90
|
+
if (evidence.relation !== 'relative_import_proxy_member') return false;
|
|
91
|
+
return targetName === binding.referencedMemberName
|
|
92
|
+
&& typeof evidence.proxyVariableName === 'string';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function relativeProvenanceValid(
|
|
96
|
+
evidence: Record<string, unknown>,
|
|
97
|
+
binding: SymbolImportReference,
|
|
98
|
+
targetName: string,
|
|
99
|
+
): boolean {
|
|
100
|
+
return directRelativeProvenance(evidence, binding, targetName)
|
|
101
|
+
|| namespaceRelativeProvenance(evidence, binding, targetName)
|
|
102
|
+
|| derivedRelativeProvenance(evidence, binding, targetName);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function relativeRows(
|
|
106
|
+
db: Db,
|
|
107
|
+
workspaceId?: number,
|
|
108
|
+
): PersistedRelativeRows {
|
|
109
|
+
const rows = db.prepare(`SELECT sc.*,r.workspace_id workspaceId
|
|
110
|
+
FROM symbol_calls sc JOIN repositories r ON r.id=sc.repo_id
|
|
111
|
+
WHERE r.fact_analyzer_version=?
|
|
112
|
+
AND (? IS NULL OR r.workspace_id=?)
|
|
113
|
+
AND (sc.import_source LIKE '.%'
|
|
114
|
+
OR json_extract(sc.evidence_json,
|
|
115
|
+
'$.importBinding.moduleKind')='relative')
|
|
116
|
+
ORDER BY sc.id`).all(ANALYZER_VERSION, workspaceId, workspaceId);
|
|
117
|
+
const parsed = rows.map(persistedRelativeCall);
|
|
118
|
+
return {
|
|
119
|
+
calls: parsed.flatMap((call) => call ? [call] : []),
|
|
120
|
+
malformedCount: parsed.filter((call) => !call).length,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function persistedRelativeCall(
|
|
125
|
+
row: Record<string, unknown>,
|
|
126
|
+
): PersistedRelativeCall | undefined {
|
|
127
|
+
const evidence = parseRecord(row.evidence_json);
|
|
128
|
+
const targetName = evidence?.targetName;
|
|
129
|
+
const binding = evidence
|
|
130
|
+
? parseRelativeImportReference(evidence.importBinding)
|
|
131
|
+
: undefined;
|
|
132
|
+
if (!evidence || !binding || typeof targetName !== 'string') return undefined;
|
|
133
|
+
if (!all([
|
|
134
|
+
typeof row.repo_id === 'number',
|
|
135
|
+
typeof row.source_file === 'string',
|
|
136
|
+
typeof row.source_line === 'number',
|
|
137
|
+
typeof row.callee_expression === 'string',
|
|
138
|
+
typeof row.call_role === 'string',
|
|
139
|
+
typeof row.status === 'string',
|
|
140
|
+
typeof evidence.caller === 'string',
|
|
141
|
+
row.import_source === binding.rawModuleSpecifier,
|
|
142
|
+
relativeProvenanceValid(evidence, binding, targetName),
|
|
143
|
+
])) return undefined;
|
|
144
|
+
return persistedCallValue(row, evidence, targetName);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function persistedCallValue(
|
|
148
|
+
row: Record<string, unknown>,
|
|
149
|
+
evidence: Record<string, unknown>,
|
|
150
|
+
targetName: string,
|
|
151
|
+
): PersistedRelativeCall {
|
|
152
|
+
return {
|
|
153
|
+
repoId: Number(row.repo_id),
|
|
154
|
+
calleeId: nullableNumber(row.callee_symbol_id),
|
|
155
|
+
status: String(row.status),
|
|
156
|
+
reason: typeof row.unresolved_reason === 'string'
|
|
157
|
+
? row.unresolved_reason : null,
|
|
158
|
+
fact: {
|
|
159
|
+
callerQualifiedName: String(evidence.caller ?? ''),
|
|
160
|
+
calleeExpression: String(row.callee_expression),
|
|
161
|
+
calleeLocalName: targetName,
|
|
162
|
+
importSource: String(row.import_source),
|
|
163
|
+
sourceFile: String(row.source_file),
|
|
164
|
+
sourceLine: Number(row.source_line),
|
|
165
|
+
callSiteStartOffset: nullableNumber(row.call_site_start_offset)
|
|
166
|
+
?? undefined,
|
|
167
|
+
callSiteEndOffset: nullableNumber(row.call_site_end_offset)
|
|
168
|
+
?? undefined,
|
|
169
|
+
callRole: row.call_role === 'event_subscribe_handler'
|
|
170
|
+
? 'event_subscribe_handler' : 'ordinary_call',
|
|
171
|
+
evidence,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function resolutionMatches(db: Db, row: PersistedRelativeCall): boolean {
|
|
177
|
+
const relation = row.fact.evidence.relation;
|
|
178
|
+
const expected = resolveRelativeSymbolCall(
|
|
179
|
+
db, row.repoId, row.fact, relation,
|
|
180
|
+
);
|
|
181
|
+
if (!expected) return false;
|
|
182
|
+
return all([
|
|
183
|
+
row.status === expected.status,
|
|
184
|
+
row.calleeId === expected.id,
|
|
185
|
+
row.reason === expected.reason,
|
|
186
|
+
row.fact.evidence.candidateStrategy === expected.strategy,
|
|
187
|
+
row.fact.evidence.candidateCount === expected.candidateCount,
|
|
188
|
+
row.fact.evidence.eligibleCandidateCount
|
|
189
|
+
=== expected.eligibleCandidateCount,
|
|
190
|
+
row.fact.evidence.selectedCandidateCount
|
|
191
|
+
=== (expected.status === 'resolved' ? 1 : 0),
|
|
192
|
+
row.fact.evidence.candidateSetComplete
|
|
193
|
+
=== expected.candidateSetComplete,
|
|
194
|
+
row.fact.evidence.resolvedModulePath
|
|
195
|
+
=== expected.resolvedModulePath,
|
|
196
|
+
row.fact.evidence.unresolvedReason === expected.reason,
|
|
197
|
+
]);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function invalidRelativeFactCategories(
|
|
201
|
+
db: Db,
|
|
202
|
+
workspaceId?: number,
|
|
203
|
+
): RelativeFactCategoryCount[] {
|
|
204
|
+
const rows = relativeRows(db, workspaceId);
|
|
205
|
+
const invalid = rows.malformedCount + rows.calls
|
|
206
|
+
.filter((row) => !resolutionMatches(db, row)).length;
|
|
207
|
+
return invalid > 0
|
|
208
|
+
? [{ category: 'relative_import_resolution_proof_invalid', count: invalid }]
|
|
209
|
+
: [];
|
|
210
|
+
}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ServiceBindingReferenceReason,
|
|
3
|
+
} from '../types.js';
|
|
4
|
+
import { ANALYZER_VERSION } from '../version.js';
|
|
5
|
+
import type { Db } from './connection.js';
|
|
6
|
+
import {
|
|
7
|
+
bindingReferenceCountsValid,
|
|
8
|
+
resolvedBindingReferenceProofValid,
|
|
9
|
+
validBindingLexicalScope,
|
|
10
|
+
type BindingProofTarget,
|
|
11
|
+
} from './012-binding-reference-proof.js';
|
|
12
|
+
import {
|
|
13
|
+
hasSingleHopHelperReturn,
|
|
14
|
+
} from './014-binding-helper-provenance.js';
|
|
15
|
+
|
|
16
|
+
export interface BindingFactCategoryCount {
|
|
17
|
+
category: string;
|
|
18
|
+
count: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface BindingCall {
|
|
22
|
+
repoId: number;
|
|
23
|
+
sourceSymbolId: number | null;
|
|
24
|
+
bindingId: number | null;
|
|
25
|
+
sourceFile: string;
|
|
26
|
+
startOffset: number;
|
|
27
|
+
endOffset: number;
|
|
28
|
+
evidence: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const unresolvedReasons = new Set<ServiceBindingReferenceReason>([
|
|
32
|
+
'binding_not_found',
|
|
33
|
+
'binding_declared_after_call',
|
|
34
|
+
'scope_chain_limit_exceeded',
|
|
35
|
+
'unsupported_reaching_assignment',
|
|
36
|
+
'unsupported_var_binding',
|
|
37
|
+
'binding_flow_unsupported',
|
|
38
|
+
]);
|
|
39
|
+
const ownerKinds =
|
|
40
|
+
"'event_registration','callback','method','object_method','function'";
|
|
41
|
+
const bindingOwnerSql = `WITH eligible AS (
|
|
42
|
+
SELECT binding.id binding_id,s.id symbol_id,
|
|
43
|
+
DENSE_RANK() OVER (PARTITION BY binding.id ORDER BY
|
|
44
|
+
s.end_offset-s.start_offset,
|
|
45
|
+
CASE s.kind WHEN 'event_registration' THEN 0 WHEN 'callback' THEN 1
|
|
46
|
+
WHEN 'method' THEN 2 WHEN 'object_method' THEN 3 ELSE 4 END,
|
|
47
|
+
s.start_offset,s.end_offset,s.qualified_name COLLATE BINARY) owner_rank
|
|
48
|
+
FROM service_bindings binding
|
|
49
|
+
JOIN repositories repo ON repo.id=binding.repo_id
|
|
50
|
+
JOIN symbols s ON s.repo_id=binding.repo_id
|
|
51
|
+
AND s.source_file=binding.source_file
|
|
52
|
+
AND s.kind IN (${ownerKinds})
|
|
53
|
+
AND s.start_offset<=binding.binding_site_start_offset
|
|
54
|
+
AND s.end_offset>=binding.binding_site_end_offset
|
|
55
|
+
WHERE repo.fact_analyzer_version=?
|
|
56
|
+
AND (? IS NULL OR repo.workspace_id=?)
|
|
57
|
+
), best AS (
|
|
58
|
+
SELECT binding_id,COUNT(*) best_count,MAX(symbol_id) symbol_id
|
|
59
|
+
FROM eligible WHERE owner_rank=1 GROUP BY binding_id
|
|
60
|
+
)
|
|
61
|
+
SELECT COUNT(*) count FROM service_bindings binding
|
|
62
|
+
JOIN repositories repo ON repo.id=binding.repo_id
|
|
63
|
+
LEFT JOIN best ON best.binding_id=binding.id
|
|
64
|
+
WHERE repo.fact_analyzer_version=?
|
|
65
|
+
AND (? IS NULL OR repo.workspace_id=?)
|
|
66
|
+
AND ((best.binding_id IS NULL AND (
|
|
67
|
+
binding.symbol_id IS NOT NULL
|
|
68
|
+
OR binding.owner_resolution<>'ownerless_file_scope'))
|
|
69
|
+
OR (best.binding_id IS NOT NULL AND (
|
|
70
|
+
best.best_count<>1 OR binding.symbol_id IS NOT best.symbol_id
|
|
71
|
+
OR binding.owner_resolution<>'owned_exact')))`;
|
|
72
|
+
|
|
73
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
74
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
75
|
+
? value as Record<string, unknown> : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseRecord(value: unknown): Record<string, unknown> | undefined {
|
|
79
|
+
if (typeof value !== 'string') return undefined;
|
|
80
|
+
try {
|
|
81
|
+
return record(JSON.parse(value) as unknown);
|
|
82
|
+
} catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function integer(value: unknown): value is number {
|
|
88
|
+
return Number.isInteger(value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function nonEmpty(value: unknown): value is string {
|
|
92
|
+
return typeof value === 'string' && value.length > 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function all(values: readonly boolean[]): boolean {
|
|
96
|
+
return values.every(Boolean);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function currentParameters(workspaceId?: number): unknown[] {
|
|
100
|
+
return [ANALYZER_VERSION, workspaceId, workspaceId];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function category(
|
|
104
|
+
name: string,
|
|
105
|
+
count: number,
|
|
106
|
+
): BindingFactCategoryCount[] {
|
|
107
|
+
return count > 0 ? [{ category: name, count }] : [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function bindingTargets(
|
|
111
|
+
db: Db,
|
|
112
|
+
workspaceId?: number,
|
|
113
|
+
): Map<number, BindingProofTarget> {
|
|
114
|
+
const rows = db.prepare(`SELECT binding.id,binding.repo_id repoId,
|
|
115
|
+
binding.symbol_id symbolId,binding.variable_name variableName,
|
|
116
|
+
binding.source_file sourceFile,
|
|
117
|
+
binding.binding_site_start_offset startOffset,
|
|
118
|
+
binding.binding_site_end_offset endOffset,
|
|
119
|
+
binding.owner_resolution ownerResolution,
|
|
120
|
+
binding.helper_chain_json helperChainJson,
|
|
121
|
+
owner.start_offset ownerStartOffset,owner.end_offset ownerEndOffset
|
|
122
|
+
FROM service_bindings binding
|
|
123
|
+
JOIN repositories repo ON repo.id=binding.repo_id
|
|
124
|
+
LEFT JOIN symbols owner ON owner.id=binding.symbol_id
|
|
125
|
+
WHERE repo.fact_analyzer_version=?
|
|
126
|
+
AND (? IS NULL OR repo.workspace_id=?)`).all(
|
|
127
|
+
...currentParameters(workspaceId),
|
|
128
|
+
);
|
|
129
|
+
return new Map(rows.flatMap(bindingTarget).map((item) => [item.id, item]));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function bindingTarget(
|
|
133
|
+
row: Record<string, unknown>,
|
|
134
|
+
): BindingProofTarget[] {
|
|
135
|
+
if (!bindingTargetShapeValid(row)) return [];
|
|
136
|
+
return [{
|
|
137
|
+
id: row.id,
|
|
138
|
+
repoId: row.repoId,
|
|
139
|
+
symbolId: row.symbolId,
|
|
140
|
+
variableName: row.variableName,
|
|
141
|
+
sourceFile: row.sourceFile,
|
|
142
|
+
startOffset: row.startOffset,
|
|
143
|
+
endOffset: row.endOffset,
|
|
144
|
+
ownerResolution: row.ownerResolution,
|
|
145
|
+
ownerStartOffset: row.ownerStartOffset,
|
|
146
|
+
ownerEndOffset: row.ownerEndOffset,
|
|
147
|
+
singleHopHelperReturn: hasSingleHopHelperReturn(row.helperChainJson),
|
|
148
|
+
}];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function nullableInteger(value: unknown): boolean {
|
|
152
|
+
return value === null || integer(value);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function bindingTargetShapeValid(
|
|
156
|
+
row: Record<string, unknown>,
|
|
157
|
+
): row is Record<string, unknown> & {
|
|
158
|
+
id: number;
|
|
159
|
+
repoId: number;
|
|
160
|
+
symbolId: number | null;
|
|
161
|
+
variableName: string;
|
|
162
|
+
sourceFile: string;
|
|
163
|
+
startOffset: number;
|
|
164
|
+
endOffset: number;
|
|
165
|
+
ownerResolution: string;
|
|
166
|
+
ownerStartOffset: number | null;
|
|
167
|
+
ownerEndOffset: number | null;
|
|
168
|
+
} {
|
|
169
|
+
return all([
|
|
170
|
+
integer(row.id), integer(row.repoId),
|
|
171
|
+
nonEmpty(row.variableName), nonEmpty(row.sourceFile),
|
|
172
|
+
integer(row.startOffset), integer(row.endOffset),
|
|
173
|
+
typeof row.ownerResolution === 'string',
|
|
174
|
+
nullableInteger(row.symbolId),
|
|
175
|
+
nullableInteger(row.ownerStartOffset),
|
|
176
|
+
nullableInteger(row.ownerEndOffset),
|
|
177
|
+
]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function bindingCalls(db: Db, workspaceId?: number): BindingCall[] {
|
|
181
|
+
const rows = db.prepare(`SELECT call.repo_id repoId,
|
|
182
|
+
call.source_symbol_id sourceSymbolId,
|
|
183
|
+
call.service_binding_id bindingId,call.source_file sourceFile,
|
|
184
|
+
call.call_site_start_offset startOffset,
|
|
185
|
+
call.call_site_end_offset endOffset,call.evidence_json evidenceJson
|
|
186
|
+
FROM outbound_calls call JOIN repositories repo ON repo.id=call.repo_id
|
|
187
|
+
WHERE repo.fact_analyzer_version=?
|
|
188
|
+
AND (? IS NULL OR repo.workspace_id=?)`).all(
|
|
189
|
+
...currentParameters(workspaceId),
|
|
190
|
+
);
|
|
191
|
+
return rows.flatMap(bindingCall);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function currentFactCount(
|
|
195
|
+
db: Db,
|
|
196
|
+
table: 'service_bindings' | 'outbound_calls',
|
|
197
|
+
workspaceId?: number,
|
|
198
|
+
): number {
|
|
199
|
+
const row = db.prepare(`SELECT COUNT(*) count FROM ${table} fact
|
|
200
|
+
JOIN repositories repo ON repo.id=fact.repo_id
|
|
201
|
+
WHERE repo.fact_analyzer_version=?
|
|
202
|
+
AND (? IS NULL OR repo.workspace_id=?)`).get(
|
|
203
|
+
...currentParameters(workspaceId),
|
|
204
|
+
);
|
|
205
|
+
return Number(row?.count ?? 0);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function bindingCall(row: Record<string, unknown>): BindingCall[] {
|
|
209
|
+
const evidence = parseRecord(row.evidenceJson);
|
|
210
|
+
if (!integer(row.repoId) || !nonEmpty(row.sourceFile)
|
|
211
|
+
|| !integer(row.startOffset) || !integer(row.endOffset) || !evidence
|
|
212
|
+
|| !(row.sourceSymbolId === null || integer(row.sourceSymbolId))
|
|
213
|
+
|| !(row.bindingId === null || integer(row.bindingId))) return [];
|
|
214
|
+
return [{
|
|
215
|
+
repoId: row.repoId,
|
|
216
|
+
sourceSymbolId: row.sourceSymbolId,
|
|
217
|
+
bindingId: row.bindingId,
|
|
218
|
+
sourceFile: row.sourceFile,
|
|
219
|
+
startOffset: row.startOffset,
|
|
220
|
+
endOffset: row.endOffset,
|
|
221
|
+
evidence,
|
|
222
|
+
}];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function rejectedFieldsAbsent(
|
|
226
|
+
reference: Record<string, unknown>,
|
|
227
|
+
): boolean {
|
|
228
|
+
return all([
|
|
229
|
+
reference.bindingSourceFile === undefined,
|
|
230
|
+
reference.bindingSiteStartOffset === undefined,
|
|
231
|
+
reference.bindingSiteEndOffset === undefined,
|
|
232
|
+
reference.resolutionStrategy === undefined,
|
|
233
|
+
reference.bindingScopeIndex === undefined,
|
|
234
|
+
]);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function rejectedChainValid(
|
|
238
|
+
reference: Record<string, unknown>,
|
|
239
|
+
): boolean {
|
|
240
|
+
const chain = reference.lexicalScopeChain;
|
|
241
|
+
if (!bindingReferenceCountsValid(reference)) return false;
|
|
242
|
+
if (Array.isArray(chain) && !chain.every(validBindingLexicalScope))
|
|
243
|
+
return false;
|
|
244
|
+
return !Array.isArray(chain)
|
|
245
|
+
|| reference.scopeChainShown === chain.length;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function rejectedReasonValid(
|
|
249
|
+
reference: Record<string, unknown>,
|
|
250
|
+
): boolean {
|
|
251
|
+
const reason = reference.reason as ServiceBindingReferenceReason;
|
|
252
|
+
const limitExceeded = reason === 'scope_chain_limit_exceeded';
|
|
253
|
+
if (limitExceeded
|
|
254
|
+
? reference.scopeChainShown !== 16
|
|
255
|
+
|| Number(reference.scopeChainOmitted) <= 0
|
|
256
|
+
: reference.scopeChainOmitted !== 0) return false;
|
|
257
|
+
if (reference.status === 'ambiguous')
|
|
258
|
+
return reason === 'binding_scope_ambiguous';
|
|
259
|
+
return reference.status === 'unresolved' && unresolvedReasons.has(reason);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function rejectedShape(
|
|
263
|
+
call: BindingCall,
|
|
264
|
+
reference: Record<string, unknown>,
|
|
265
|
+
): boolean {
|
|
266
|
+
return all([
|
|
267
|
+
nonEmpty(reference.variableName),
|
|
268
|
+
call.bindingId === null,
|
|
269
|
+
rejectedFieldsAbsent(reference),
|
|
270
|
+
rejectedChainValid(reference),
|
|
271
|
+
rejectedReasonValid(reference),
|
|
272
|
+
]);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function notApplicableShape(
|
|
276
|
+
call: BindingCall,
|
|
277
|
+
reference: Record<string, unknown>,
|
|
278
|
+
): boolean {
|
|
279
|
+
return all([
|
|
280
|
+
call.bindingId === null,
|
|
281
|
+
reference.variableName === undefined,
|
|
282
|
+
reference.reason === undefined,
|
|
283
|
+
rejectedFieldsAbsent(reference),
|
|
284
|
+
reference.lexicalScopeChain === undefined,
|
|
285
|
+
bindingReferenceCountsValid(reference),
|
|
286
|
+
reference.scopeChainTotal === 0,
|
|
287
|
+
reference.scopeChainShown === 0,
|
|
288
|
+
reference.scopeChainOmitted === 0,
|
|
289
|
+
]);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function resolutionEvidenceValid(
|
|
293
|
+
call: BindingCall,
|
|
294
|
+
reference: Record<string, unknown>,
|
|
295
|
+
): boolean {
|
|
296
|
+
const resolution = record(call.evidence.serviceBindingResolution);
|
|
297
|
+
if (!resolution) return false;
|
|
298
|
+
if (reference.status === 'resolved_exact')
|
|
299
|
+
return resolution.status === 'selected_exact'
|
|
300
|
+
&& resolution.selectedBindingId === call.bindingId
|
|
301
|
+
&& resolution.candidateCount === 1;
|
|
302
|
+
return resolution.status === reference.status
|
|
303
|
+
&& resolution.candidateCount === 0
|
|
304
|
+
&& resolution.selectedBindingId === undefined;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function referenceValid(
|
|
308
|
+
call: BindingCall,
|
|
309
|
+
targets: ReadonlyMap<number, BindingProofTarget>,
|
|
310
|
+
): boolean {
|
|
311
|
+
const reference = record(call.evidence.serviceBindingReference);
|
|
312
|
+
if (!reference || !resolutionEvidenceValid(call, reference)) return false;
|
|
313
|
+
if (reference.status === 'not_applicable')
|
|
314
|
+
return notApplicableShape(call, reference);
|
|
315
|
+
if (reference.status === 'ambiguous' || reference.status === 'unresolved')
|
|
316
|
+
return rejectedShape(call, reference);
|
|
317
|
+
if (reference.status !== 'resolved_exact') return false;
|
|
318
|
+
return resolvedBindingReferenceProofValid(
|
|
319
|
+
call,
|
|
320
|
+
reference,
|
|
321
|
+
call.bindingId === null ? undefined : targets.get(call.bindingId),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function invalidOwnerCount(db: Db, workspaceId?: number): number {
|
|
326
|
+
const params = currentParameters(workspaceId);
|
|
327
|
+
const row = db.prepare(bindingOwnerSql).get(...params, ...params);
|
|
328
|
+
return Number(row?.count ?? 0);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function invalidBindingFactCategories(
|
|
332
|
+
db: Db,
|
|
333
|
+
workspaceId?: number,
|
|
334
|
+
): BindingFactCategoryCount[] {
|
|
335
|
+
const targets = bindingTargets(db, workspaceId);
|
|
336
|
+
const calls = bindingCalls(db, workspaceId);
|
|
337
|
+
const invalidReferences = calls.filter((call) =>
|
|
338
|
+
!referenceValid(call, targets)).length;
|
|
339
|
+
const malformedTargets = currentFactCount(
|
|
340
|
+
db, 'service_bindings', workspaceId,
|
|
341
|
+
) - targets.size;
|
|
342
|
+
const malformedCalls = currentFactCount(
|
|
343
|
+
db, 'outbound_calls', workspaceId,
|
|
344
|
+
) - calls.length;
|
|
345
|
+
return [
|
|
346
|
+
...category('service_binding_row_shape_invalid', malformedTargets),
|
|
347
|
+
...category('outbound_binding_row_shape_invalid', malformedCalls),
|
|
348
|
+
...category('service_binding_unique_owner_invalid',
|
|
349
|
+
invalidOwnerCount(db, workspaceId)),
|
|
350
|
+
...category('outbound_binding_lexical_proof_invalid', invalidReferences),
|
|
351
|
+
];
|
|
352
|
+
}
|