ucn 5.3.4 → 5.3.5
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/.claude/skills/ucn/SKILL.md +22 -0
- package/core/account.js +5 -1
- package/core/analysis.js +18 -0
- package/core/cache.js +4 -1
- package/core/callers.js +939 -122
- package/core/confidence.js +26 -14
- package/core/imports.js +18 -4
- package/core/index-ir.js +1 -1
- package/core/ir.js +1 -1
- package/core/output/analysis.js +20 -8
- package/core/output/provenance.js +41 -0
- package/core/output/public.js +2 -1
- package/core/output/shared.js +3 -0
- package/core/provenance-facts.js +287 -0
- package/core/provenance-overload.js +118 -0
- package/core/provenance.js +423 -0
- package/core/python-fixture-flow.js +216 -0
- package/core/receiver-types.js +26 -0
- package/core/rust-result-flow.js +206 -0
- package/core/verify.js +7 -3
- package/languages/adapter.js +2 -0
- package/languages/c-family.js +14 -1
- package/languages/csharp.js +21 -9
- package/languages/go.js +105 -86
- package/languages/java.js +23 -10
- package/languages/javascript.js +129 -18
- package/languages/python.js +202 -35
- package/languages/rust.js +173 -73
- package/languages/type-evidence.js +63 -0
- package/package.json +2 -2
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { langTraits } = require('../languages');
|
|
5
|
+
const { NON_CALLABLE_TYPES } = require('./shared');
|
|
6
|
+
const { declarationIdentity, identityKey, propertyReadMember } = require('./provenance');
|
|
7
|
+
const { captureOverload, overloadMemberGroup } = require('./provenance-overload');
|
|
8
|
+
const { splitParentList } = require('./graph-build');
|
|
9
|
+
|
|
10
|
+
const TYPE_KINDS = new Set(['class', 'struct', 'interface', 'trait', 'record', 'enum', 'type', 'impl']);
|
|
11
|
+
const ownerName = definition => definition.className || (definition.receiver || '').replace(/^\*/, '');
|
|
12
|
+
|
|
13
|
+
function occurrenceIdentity(file, call, siteId) {
|
|
14
|
+
const position = call.callSite || {};
|
|
15
|
+
const column = call.column ?? position.column;
|
|
16
|
+
const start = call.callStart ?? position.start;
|
|
17
|
+
const end = call.callEnd ?? position.end;
|
|
18
|
+
return {
|
|
19
|
+
file, line: call.line,
|
|
20
|
+
...(Number.isInteger(column) && { column }),
|
|
21
|
+
...(Number.isInteger(start) && { start }),
|
|
22
|
+
...(Number.isInteger(end) && { end }),
|
|
23
|
+
...(siteId !== undefined && { siteId }),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Follow paired AST import/export names; an arbitrary file import is no hop. */
|
|
28
|
+
function namedDeclaration(index, file, name, accepts, line, seen = new Set()) {
|
|
29
|
+
const key = `${file}\0${name}`;
|
|
30
|
+
if (seen.has(key) || seen.size >= 8) return null;
|
|
31
|
+
const visited = new Set(seen).add(key);
|
|
32
|
+
const entry = index.files.get(file);
|
|
33
|
+
if (!entry) return null;
|
|
34
|
+
const separator = name.includes('::') ? '::' : '.';
|
|
35
|
+
const parts = name.split(separator);
|
|
36
|
+
if (parts.length === 2) {
|
|
37
|
+
const binding = (entry.importBindings || []).find(b => (b.alias || b.name) === parts[0]);
|
|
38
|
+
const relative = binding && entry.moduleResolved?.[binding.module];
|
|
39
|
+
if (relative) {
|
|
40
|
+
const result = namedDeclaration(index, path.resolve(index.root, relative), parts[1], accepts, undefined, visited);
|
|
41
|
+
if (result) return { declaration: result.declaration, chain: [{
|
|
42
|
+
fromFile: entry.relativePath, toFile: relative, localName: name,
|
|
43
|
+
importedName: parts[1], module: binding.module,
|
|
44
|
+
...(!result.chain.length && { declaration: declarationIdentity(result.declaration) }),
|
|
45
|
+
}, ...result.chain] };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
let local = (index.symbols.get(name) || []).filter(d => d.file === file && accepts(d));
|
|
49
|
+
if (line != null) {
|
|
50
|
+
local = local.filter(d => !d.lexicalScopeStartLine ||
|
|
51
|
+
(line >= d.lexicalScopeStartLine && line <= d.lexicalScopeEndLine));
|
|
52
|
+
const scoped = local.filter(d => d.lexicalScopeStartLine);
|
|
53
|
+
if (scoped.length) {
|
|
54
|
+
const nearest = Math.max(...scoped.map(d => d.lexicalScopeStartLine));
|
|
55
|
+
local = scoped.filter(d => d.lexicalScopeStartLine === nearest);
|
|
56
|
+
}
|
|
57
|
+
if (local.length > 1) {
|
|
58
|
+
const enclosing = index.findEnclosingFunction(file, line, true);
|
|
59
|
+
const inScope = enclosing && local.filter(d =>
|
|
60
|
+
d.startLine >= enclosing.startLine && d.endLine <= enclosing.endLine && d.startLine <= line);
|
|
61
|
+
if (inScope?.length === 1) local = inScope;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (local.length) return local.length === 1 ? { declaration: local[0], chain: [] } : null;
|
|
65
|
+
const localExports = (entry.exportDetails || []).filter(e =>
|
|
66
|
+
!e.source && (e.alias || e.name) === name && (e.localName || e.name) !== name);
|
|
67
|
+
if (localExports.length === 1) {
|
|
68
|
+
const importedName = localExports[0].localName || localExports[0].name;
|
|
69
|
+
const result = namedDeclaration(index, file, importedName, accepts, line, visited);
|
|
70
|
+
if (result) return { declaration: result.declaration, chain: [{
|
|
71
|
+
fromFile: entry.relativePath, toFile: entry.relativePath,
|
|
72
|
+
localName: name, importedName,
|
|
73
|
+
...(!result.chain.length && { declaration: declarationIdentity(result.declaration) }),
|
|
74
|
+
}, ...result.chain] };
|
|
75
|
+
}
|
|
76
|
+
const aliases = entry.importAliases || [];
|
|
77
|
+
const alias = aliases.find(a => a.local === name);
|
|
78
|
+
const original = alias?.original || name;
|
|
79
|
+
const links = (entry.importBindings || []).filter(b =>
|
|
80
|
+
b.alias === name || (b.name === original && (!b.alias || b.alias === name)));
|
|
81
|
+
const exports = (entry.exportDetails || []).filter(e =>
|
|
82
|
+
e.type === 're-export' && (e.alias || e.name) === name && e.source);
|
|
83
|
+
const found = [];
|
|
84
|
+
for (const link of [...links, ...exports]) {
|
|
85
|
+
const module = link.module || link.source;
|
|
86
|
+
const relative = entry.moduleResolved?.[module];
|
|
87
|
+
if (!relative) continue;
|
|
88
|
+
const toFile = path.resolve(index.root, relative);
|
|
89
|
+
const importedName = link.originalName || link.imported || link.name;
|
|
90
|
+
if (!importedName || importedName === '*') continue;
|
|
91
|
+
const result = namedDeclaration(index, toFile, importedName, accepts, undefined, visited);
|
|
92
|
+
if (!result) continue;
|
|
93
|
+
const hop = {
|
|
94
|
+
fromFile: entry.relativePath, toFile: relative,
|
|
95
|
+
localName: name, importedName, module, line: link.line,
|
|
96
|
+
...(!result.chain.length && { declaration: declarationIdentity(result.declaration) }),
|
|
97
|
+
};
|
|
98
|
+
found.push({ declaration: result.declaration, chain: [hop, ...result.chain] });
|
|
99
|
+
}
|
|
100
|
+
const distinct = new Map(found.map(f => [identityKey(declarationIdentity(f.declaration)), f]));
|
|
101
|
+
return distinct.size === 1 ? distinct.values().next().value : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Collect independent facts; a failed lookup never manufactures a negative. */
|
|
105
|
+
function confirmationFacts(index, file, call, targets, options = {}) {
|
|
106
|
+
const entry = index.files.get(file);
|
|
107
|
+
const language = entry?.language;
|
|
108
|
+
const facts = {
|
|
109
|
+
language,
|
|
110
|
+
site: occurrenceIdentity(entry?.relativePath || file, call),
|
|
111
|
+
targets: targets.map(declarationIdentity),
|
|
112
|
+
receiver: call.receiver || null,
|
|
113
|
+
receiverType: options.receiverType || call.receiverType || null,
|
|
114
|
+
receiverTypeSource: options.receiverTypeSource || call.receiverTypeSource || 'unknown',
|
|
115
|
+
receiverOrigin: options.receiverOrigin || call.receiverTypeEvidence || null,
|
|
116
|
+
...(call.isFunctionReference && { valueReference: true }),
|
|
117
|
+
ownerCount: new Set((index.symbols.get(call.name) || [])
|
|
118
|
+
.filter(d => !NON_CALLABLE_TYPES.has(d.type) && ownerName(d))
|
|
119
|
+
.map(d => `${d.file}\0${ownerName(d)}\0${d.namespace || ''}`)).size,
|
|
120
|
+
...((options.originFile || call.receiverTypeFlowFile) && {
|
|
121
|
+
receiverTypeFlowFile: path.relative(index.root, options.originFile || call.receiverTypeFlowFile),
|
|
122
|
+
}),
|
|
123
|
+
...(call.moduleOwnedPath && { moduleOwnedPath: true }),
|
|
124
|
+
};
|
|
125
|
+
if (facts.receiverTypeSource === 'fixture' || facts.receiverOrigin?.externalFactory) facts.receiverPath = call.receiverRoot
|
|
126
|
+
? [call.receiverRoot, ...(call.receiverFields || [call.receiverField])]
|
|
127
|
+
: [call.receiver];
|
|
128
|
+
const methodReceiver = call.isMethod && !call.moduleOwnedPath;
|
|
129
|
+
// Method-name bindings cannot establish the identity of a value receiver.
|
|
130
|
+
if (options.bindingId && !methodReceiver) {
|
|
131
|
+
const bound = (index.symbols.get(call.resolvedName || call.name) || [])
|
|
132
|
+
.filter(d => d.bindingId === options.bindingId);
|
|
133
|
+
if (bound.length === 1) facts.binding = {
|
|
134
|
+
referenceId: options.bindingId, declaration: declarationIdentity(bound[0]),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (!methodReceiver) {
|
|
138
|
+
const result = namedDeclaration(index, file, call.resolvedName || call.name,
|
|
139
|
+
d => !NON_CALLABLE_TYPES.has(d.type) || call.isConstructor, call.line);
|
|
140
|
+
if (result?.chain.length) facts.importChain = result.chain;
|
|
141
|
+
}
|
|
142
|
+
let type = facts.receiverType;
|
|
143
|
+
if (options.sameClass) {
|
|
144
|
+
const enclosing = index.findEnclosingFunction(file, call.line, true);
|
|
145
|
+
type = enclosing?.className || enclosing?.receiver?.replace(/^\*/, '');
|
|
146
|
+
facts.receiverTypeSource = 'same-class';
|
|
147
|
+
facts.receiverOrigin = { source: 'same-class', declaration: declarationIdentity(enclosing) };
|
|
148
|
+
}
|
|
149
|
+
if (language === 'csharp' && call.receiverCastThis) {
|
|
150
|
+
const enclosing = index.findEnclosingFunction(file, call.line, true);
|
|
151
|
+
if (enclosing?.className) {
|
|
152
|
+
facts.receiverCastThis = { enclosing: declarationIdentity(enclosing), interfaceType: type };
|
|
153
|
+
type = enclosing.className;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!type) {
|
|
157
|
+
facts.incomplete = ['receiverType'];
|
|
158
|
+
return facts;
|
|
159
|
+
}
|
|
160
|
+
if (facts.receiverTypeSource === 'unknown') facts.incomplete = ['receiverTypeSource'];
|
|
161
|
+
const originFile = options.originFile || call.receiverTypeFlowFile || file;
|
|
162
|
+
const resolveType = (name, context, atLine, qualifier) => {
|
|
163
|
+
// A qualified annotation cannot bind to a same-named local type.
|
|
164
|
+
// Apply the qualifier only to the receiver's first lookup: aliases
|
|
165
|
+
// and parent annotations are resolved in their own declaration scope.
|
|
166
|
+
if (qualifier && options.resolveType &&
|
|
167
|
+
langTraits(language)?.typeSystem !== 'structural') {
|
|
168
|
+
const definition = options.resolveType(name, context, atLine, qualifier);
|
|
169
|
+
return definition ? { declaration: definition, chain: [] } : null;
|
|
170
|
+
}
|
|
171
|
+
const named = namedDeclaration(index, context, name, d =>
|
|
172
|
+
(TYPE_KINDS.has(d.type) && d.type !== 'impl') ||
|
|
173
|
+
(['javascript', 'typescript', 'tsx'].includes(language) &&
|
|
174
|
+
d.type === 'function' && !d.className &&
|
|
175
|
+
['constructor', 'flow'].includes(facts.receiverTypeSource)), atLine);
|
|
176
|
+
if (named) return named;
|
|
177
|
+
// Nominal package/type lookup is delegated to the engine's language
|
|
178
|
+
// rules, then pinned to a unique declaration in the resolved scope.
|
|
179
|
+
if (langTraits(language)?.typeSystem !== 'structural' && options.resolveType) {
|
|
180
|
+
const definition = options.resolveType(name, context, atLine);
|
|
181
|
+
if (definition) return { declaration: definition, chain: [] };
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
};
|
|
185
|
+
const qualifier = call.receiverTypeNamespace || call.receiverTypeQualifier;
|
|
186
|
+
if (qualifier) facts.receiverTypeQualifier = qualifier;
|
|
187
|
+
// A parser annotation's qualifier belongs to the consuming file. The
|
|
188
|
+
// already-pinned declaration file may be another crate/module, where
|
|
189
|
+
// replaying `crate::Type` or an import alias would change its meaning.
|
|
190
|
+
const receiverContext = qualifier && facts.receiverTypeSource === 'annotation' ? file : originFile;
|
|
191
|
+
let resolved = resolveType(type, receiverContext, receiverContext === file ? call.line : undefined, qualifier);
|
|
192
|
+
if (resolved && receiverContext !== originFile && resolved.declaration.file !== originFile) resolved = null;
|
|
193
|
+
if (!resolved && language === 'rust') {
|
|
194
|
+
const enclosing = index.findEnclosingFunction(file, call.line, true);
|
|
195
|
+
const bounds = enclosing?.genericBounds?.[type];
|
|
196
|
+
const boundTypes = (bounds || []).map(bound => resolveType(bound, file, call.line))
|
|
197
|
+
.filter(bound => bound?.declaration.type === 'trait' &&
|
|
198
|
+
(index.symbols.get(call.name) || []).some(member =>
|
|
199
|
+
ownerName(member) === bound.declaration.name && member.file === bound.declaration.file));
|
|
200
|
+
if (boundTypes.length === 1) {
|
|
201
|
+
resolved = boundTypes[0];
|
|
202
|
+
facts.receiverGenericBound = { declaration: declarationIdentity(enclosing),
|
|
203
|
+
parameter: type, bounds, selected: declarationIdentity(resolved.declaration) };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (!resolved) return facts;
|
|
207
|
+
facts.receiverTypeDeclaration = declarationIdentity(resolved.declaration);
|
|
208
|
+
if (resolved.chain.length) facts.receiverImportChain = resolved.chain;
|
|
209
|
+
const aliases = [];
|
|
210
|
+
while (resolved.declaration.aliasOf && aliases.length < 8) {
|
|
211
|
+
const alias = resolved.declaration;
|
|
212
|
+
const head = options.typeHead?.(alias.aliasOf) || alias.aliasOf;
|
|
213
|
+
const target = resolveType(head, alias.file);
|
|
214
|
+
if (!target || aliases.some(a => a.declaration.file === alias.relativePath && a.declaration.startLine === alias.startLine)) return facts;
|
|
215
|
+
aliases.push({ declaration: declarationIdentity(alias), aliasOf: alias.aliasOf,
|
|
216
|
+
name: head, target: declarationIdentity(target.declaration) });
|
|
217
|
+
resolved = target;
|
|
218
|
+
}
|
|
219
|
+
if (aliases.length) facts.receiverAliases = aliases;
|
|
220
|
+
facts.receiverResolvedIn = declarationIdentity(resolved.declaration);
|
|
221
|
+
const visited = new Set();
|
|
222
|
+
const walk = (owner, steps) => {
|
|
223
|
+
const identity = declarationIdentity(owner);
|
|
224
|
+
const key = identityKey(identity);
|
|
225
|
+
if (visited.has(key) || visited.size >= 16) return null;
|
|
226
|
+
visited.add(key);
|
|
227
|
+
const members = (index.symbols.get(call.name) || []).filter(d => {
|
|
228
|
+
if (NON_CALLABLE_TYPES.has(d.type) || ownerName(d) !== owner.name) return false;
|
|
229
|
+
if (language === 'csharp' && d.explicitInterface &&
|
|
230
|
+
(!facts.receiverCastThis || d.explicitInterface !== facts.receiverCastThis.interfaceType)) return false;
|
|
231
|
+
if ((d.namespace || null) !== (owner.namespace || null)) return false;
|
|
232
|
+
if ((d.enclosingType || null) !== (owner.enclosingType || null)) return false;
|
|
233
|
+
return d.file === owner.file || (language === 'go' &&
|
|
234
|
+
path.dirname(d.file) === path.dirname(owner.file));
|
|
235
|
+
});
|
|
236
|
+
// Use the AST spelling before the inheritance graph normalizes aliases
|
|
237
|
+
// and qualifiers. A terminal name alone loses its declaring scope.
|
|
238
|
+
const rawOwner = index.files.get(owner.file)?.symbols.find(d =>
|
|
239
|
+
d.name === owner.name && d.startLine === owner.startLine && d.type === owner.type) || owner;
|
|
240
|
+
const parentNames = rawOwner.extends ? splitParentList(rawOwner.extends) : [];
|
|
241
|
+
const derefTarget = language === 'rust' ? rawOwner.derefTarget : null;
|
|
242
|
+
const parents = [...parentNames, ...(derefTarget ? [derefTarget] : [])]
|
|
243
|
+
.map(name => resolveType(name, owner.file, owner.startLine)).filter(Boolean).map(r => r.declaration);
|
|
244
|
+
// The member may be inherited from an external declaration. This
|
|
245
|
+
// collector has no platform-member lookup; report the coverage gap
|
|
246
|
+
// separately from an ambiguous or inconsistent project lookup.
|
|
247
|
+
if (!members.length && parents.length < parentNames.length) {
|
|
248
|
+
facts.lookupUnsupported = 'external-parent-member';
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
const step = { owner: identity, members: members.map(declarationIdentity), parents: parents.map(declarationIdentity),
|
|
252
|
+
...(derefTarget && { derefTarget }) };
|
|
253
|
+
const getter = facts.valueReference && propertyReadMember(step.members);
|
|
254
|
+
if (getter) return { receiver: declarationIdentity(resolved.declaration),
|
|
255
|
+
steps: [...steps, { ...step, propertyRead: true }], selected: getter };
|
|
256
|
+
if (language === 'java' || language === 'csharp') {
|
|
257
|
+
step.memberDefinitions = members;
|
|
258
|
+
const chain = [...steps, step];
|
|
259
|
+
if (parents.length === 1) return walk(parents[0], chain);
|
|
260
|
+
if (parents.length > 1) return null;
|
|
261
|
+
const group = overloadMemberGroup(chain);
|
|
262
|
+
if (!group?.length) return null;
|
|
263
|
+
const overload = captureOverload(index, call, group, language, options.selectOverload);
|
|
264
|
+
if (!overload) return null;
|
|
265
|
+
return { receiver: declarationIdentity(resolved.declaration), steps: chain,
|
|
266
|
+
selected: overload.selected || declarationIdentity(group[0]), overload };
|
|
267
|
+
}
|
|
268
|
+
if (members.length === 1) return {
|
|
269
|
+
receiver: declarationIdentity(resolved.declaration),
|
|
270
|
+
steps: [...steps, step], selected: declarationIdentity(members[0]),
|
|
271
|
+
};
|
|
272
|
+
if (members.length > 1 && options.selectOverload) {
|
|
273
|
+
const overload = captureOverload(index, call, members, language, options.selectOverload);
|
|
274
|
+
if (overload?.selected) return {
|
|
275
|
+
receiver: declarationIdentity(resolved.declaration),
|
|
276
|
+
steps: [...steps, { ...step, overload }], selected: overload.selected,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (members.length || parents.length !== 1) return null;
|
|
280
|
+
return walk(parents[0], [...steps, step]);
|
|
281
|
+
};
|
|
282
|
+
const lookup = walk(resolved.declaration, []);
|
|
283
|
+
if (lookup) facts.lookup = lookup;
|
|
284
|
+
return facts;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = { confirmationFacts, namedDeclaration, occurrenceIdentity };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { declarationIdentity, sameDeclaration } = require('./provenance');
|
|
4
|
+
|
|
5
|
+
// Materialize the declaration/ancestry reads used by overload selection.
|
|
6
|
+
// Rechecking a witness runs the same selector over these data, with no live
|
|
7
|
+
// index, filesystem, or name-ranking fallback. An unrecorded read abstains.
|
|
8
|
+
function encode(value) {
|
|
9
|
+
if (value === undefined) return { $undefined: true };
|
|
10
|
+
if (value instanceof Set) return { $set: [...value].map(encode) };
|
|
11
|
+
if (value instanceof Map) return { $map: [...value].map(([k, v]) => [encode(k), encode(v)]) };
|
|
12
|
+
if (Array.isArray(value)) return value.map(encode);
|
|
13
|
+
if (value && typeof value === 'object') return Object.fromEntries(
|
|
14
|
+
Object.entries(value).filter(([, v]) => typeof v !== 'function').map(([k, v]) => [k, encode(v)]));
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function decode(value) {
|
|
18
|
+
if (value?.$undefined) return undefined;
|
|
19
|
+
if (value?.$set) return new Set(value.$set.map(decode));
|
|
20
|
+
if (value?.$map) return new Map(value.$map.map(([k, v]) => [decode(k), decode(v)]));
|
|
21
|
+
if (Array.isArray(value)) return value.map(decode);
|
|
22
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, decode(v)]));
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function factIndex(reads, live) {
|
|
27
|
+
const local = new Map();
|
|
28
|
+
const read = (key, compute) => {
|
|
29
|
+
if (live && !Object.hasOwn(reads, key)) reads[key] = encode(compute());
|
|
30
|
+
if (!Object.hasOwn(reads, key)) throw new Error(`Missing overload fact: ${key}`);
|
|
31
|
+
return decode(reads[key]);
|
|
32
|
+
};
|
|
33
|
+
return new Proxy({}, {
|
|
34
|
+
get(_, property) {
|
|
35
|
+
if (local.has(property)) return local.get(property);
|
|
36
|
+
if (String(property).includes('Cache')) {
|
|
37
|
+
const cache = new Map(); local.set(property, cache); return cache;
|
|
38
|
+
}
|
|
39
|
+
if (['symbols', 'files', 'importGraph', 'extendsGraph', 'extendedByGraph'].includes(property)) {
|
|
40
|
+
return {
|
|
41
|
+
get(key) {
|
|
42
|
+
return read(`${property}.get:${JSON.stringify(key)}`, () => {
|
|
43
|
+
const value = live[property]?.get(key);
|
|
44
|
+
if (property !== 'files' || !value) return value;
|
|
45
|
+
return { language: value.language, relativePath: value.relativePath,
|
|
46
|
+
importBindings: value.importBindings, moduleResolved: value.moduleResolved };
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
has(key) { return !!this.get(key); },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const method = live ? typeof live[property] === 'function'
|
|
53
|
+
: reads[`method:${String(property)}`] === true;
|
|
54
|
+
if (method) {
|
|
55
|
+
if (live) reads[`method:${String(property)}`] = true;
|
|
56
|
+
return (...args) => read(`${String(property)}:${JSON.stringify(args)}`,
|
|
57
|
+
() => live[property](...args));
|
|
58
|
+
}
|
|
59
|
+
return read(`property:${String(property)}`, () => live[property]);
|
|
60
|
+
},
|
|
61
|
+
set(_, property, value) { local.set(property, value); return true; },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function captureOverload(index, call, candidates, language, select) {
|
|
66
|
+
const reads = {};
|
|
67
|
+
try {
|
|
68
|
+
const result = select(factIndex(reads, index), call, candidates, language);
|
|
69
|
+
return { call: encode(call), candidates: encode(candidates), language, reads,
|
|
70
|
+
selected: declarationIdentity(result.match),
|
|
71
|
+
outcome: result.match ? 'selected' : result.ambiguous ? 'ambiguous' : 'no-fit' };
|
|
72
|
+
} catch { return null; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function validateOverload(witness, members, selected, invalidCall = false, ambiguous = false) {
|
|
76
|
+
if (!witness || !Array.isArray(witness.candidates)) return false;
|
|
77
|
+
const candidates = decode(witness.candidates);
|
|
78
|
+
if (candidates.length !== members.length || candidates.some(candidate =>
|
|
79
|
+
!members.some(member => sameDeclaration(member, declarationIdentity(candidate))))) return false;
|
|
80
|
+
try {
|
|
81
|
+
// Lazy access avoids an initialization cycle with confidence.js.
|
|
82
|
+
// The selector only sees the data-only replay facade above.
|
|
83
|
+
const select = require('./callers').selectProvenanceOverload;
|
|
84
|
+
const result = select(factIndex(witness.reads), decode(witness.call), candidates, witness.language);
|
|
85
|
+
if (ambiguous) return !result.match && result.ambiguous === true;
|
|
86
|
+
return invalidCall ? !result.match && !result.ambiguous :
|
|
87
|
+
!!result.match && sameDeclaration(declarationIdentity(result.match), selected);
|
|
88
|
+
} catch { return false; }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Reconstruct inherited overload slots from the actual declarations, with
|
|
92
|
+
// the nearest declaration of an identical signature hiding the ancestor.
|
|
93
|
+
function overloadMemberGroup(steps) {
|
|
94
|
+
const normalize = require('./callers').provenanceParameterIdentity;
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
const group = [];
|
|
97
|
+
for (const step of steps) {
|
|
98
|
+
if (!Array.isArray(step.memberDefinitions) ||
|
|
99
|
+
step.memberDefinitions.length !== step.members.length ||
|
|
100
|
+
step.memberDefinitions.some(d => !step.members.some(m =>
|
|
101
|
+
sameDeclaration(m, declarationIdentity(d))))) return null;
|
|
102
|
+
const sameDepth = [];
|
|
103
|
+
for (const definition of step.memberDefinitions) {
|
|
104
|
+
const signature = Array.isArray(definition.paramsStructured)
|
|
105
|
+
? definition.paramsStructured.filter(p => !p?.extensionReceiver)
|
|
106
|
+
.map(p => `${normalize(p?.type || '')}:${p?.rest ? 'rest' : 'fixed'}`).join(',')
|
|
107
|
+
: `${definition.file}:${definition.startLine}`;
|
|
108
|
+
if (!seen.has(signature)) {
|
|
109
|
+
group.push(definition);
|
|
110
|
+
sameDepth.push(signature);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (const signature of sameDepth) seen.add(signature);
|
|
114
|
+
}
|
|
115
|
+
return group;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { captureOverload, validateOverload, overloadMemberGroup };
|