ucn 5.5.1 → 5.5.2
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 +35 -9
- package/.claude/skills/ucn/references/commands.md +38 -16
- package/.claude/skills/ucn/references/resolution-rules.md +126 -20
- package/.claude/skills/ucn/references/trust-contract.md +5 -3
- package/README.md +30 -21
- package/cli/index.js +1 -1
- package/core/accessors.js +341 -54
- package/core/account.js +74 -20
- package/core/analysis.js +257 -125
- package/core/ast-analysis.js +12 -3
- package/core/brief.js +8 -4
- package/core/build-worker.js +2 -1
- package/core/cache.js +137 -6
- package/core/call-name-cache.js +134 -0
- package/core/callable-scope.js +21 -0
- package/core/callers.js +4726 -940
- package/core/check.js +22 -5
- package/core/class-identity.js +399 -12
- package/core/constructor-calls.js +831 -0
- package/core/constructor-hierarchy.js +147 -0
- package/core/constructor-protocol.js +196 -0
- package/core/contract-membership.js +346 -140
- package/core/cpp-conversion-constraints.js +123 -0
- package/core/cpp-conversion-syntax.js +216 -0
- package/core/cpp-conversions.js +414 -0
- package/core/cpp-scope.js +245 -44
- package/core/deadcode.js +35 -15
- package/core/decorator-binding.js +137 -3
- package/core/enum-reference-syntax.js +134 -0
- package/core/execute.js +17 -1
- package/core/external-macros.js +42 -10
- package/core/go-packages.js +171 -0
- package/core/graph-build.js +2 -0
- package/core/graph.js +75 -59
- package/core/ignore-rules.js +2 -1
- package/core/imports.js +145 -19
- package/core/index-ir.js +41 -4
- package/core/ir.js +27 -6
- package/core/macro-expansion.js +38 -2
- package/core/out-of-class-members.js +182 -0
- package/core/output/analysis-ext.js +4 -2
- package/core/output/analysis.js +89 -20
- package/core/output/graph.js +12 -3
- package/core/output/lines.js +4 -3
- package/core/output/public.js +1 -0
- package/core/output/reporting.js +6 -1
- package/core/output/shared.js +2 -0
- package/core/output/tracing.js +28 -22
- package/core/output-budget.js +2 -2
- package/core/parallel-build.js +52 -1
- package/core/project.js +217 -112
- package/core/provenance-constructor-base.js +161 -0
- package/core/provenance-facts.js +257 -27
- package/core/provenance-overload.js +458 -54
- package/core/provenance-type.js +487 -0
- package/core/provenance.js +234 -15
- package/core/public-command.js +17 -2
- package/core/query-bodies.js +159 -0
- package/core/reflection.js +255 -2
- package/core/rename-token-syntax.js +111 -0
- package/core/reporting.js +13 -7
- package/core/rust-macro-expansion.js +151 -19
- package/core/rust-modules.js +1 -1
- package/core/rust-names.js +607 -0
- package/core/search.js +70 -17
- package/core/shared.js +98 -1
- package/core/source-memo.js +201 -0
- package/core/text-usage-syntax.js +38 -0
- package/core/tracing.js +7 -1
- package/core/type-denotation.js +124 -37
- package/core/type-references.js +615 -246
- package/core/value-references.js +2777 -0
- package/core/verify.js +1097 -281
- package/languages/adapter.js +28 -4
- package/languages/c-family.js +1118 -164
- package/languages/constructor-bases.js +105 -0
- package/languages/cpp-platform.js +26 -0
- package/languages/cpp-site-syntax.js +83 -0
- package/languages/csharp-scope.js +439 -0
- package/languages/csharp.js +428 -68
- package/languages/go.js +385 -207
- package/languages/html.js +4 -2
- package/languages/index.js +397 -2
- package/languages/java-platform.js +226 -0
- package/languages/java-unicode.js +269 -0
- package/languages/java.js +461 -117
- package/languages/javascript.js +418 -66
- package/languages/lexical-scope.js +190 -23
- package/languages/python.js +404 -54
- package/languages/rust.js +510 -82
- package/languages/utils.js +67 -8
- package/mcp/server.js +8 -2
- package/package.json +2 -2
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { langTraits, getLanguageAdapter, getParser } = require('../languages');
|
|
4
|
+
const { declarationIdentity, sameDeclaration } = require('./provenance');
|
|
5
|
+
const { occurrenceIdentity } = require('./provenance-facts');
|
|
6
|
+
const { captureTypeLookup, validateTypeLookup } = require('./provenance-type');
|
|
7
|
+
const { SourceMemo } = require('./source-memo');
|
|
8
|
+
|
|
9
|
+
// Omitted-initializer call syntax of a source, by language.
|
|
10
|
+
const sourceCalls = new SourceMemo({ maxEntries: 128, maxWeight: 32 * 1024 * 1024,
|
|
11
|
+
weigh: calls => JSON.stringify(calls).length * 2 });
|
|
12
|
+
|
|
13
|
+
const implicitRecord = call => ({
|
|
14
|
+
name: call.name, qualifier: call.constructorQualifier || null,
|
|
15
|
+
argCount: call.argCount, site: occurrenceIdentity('', call),
|
|
16
|
+
base: JSON.stringify(call.implicitBase), enclosing: enclosingKey(call.enclosingFunction),
|
|
17
|
+
header: call.implicitBase?.header && { ...call.implicitBase.header },
|
|
18
|
+
});
|
|
19
|
+
// The constructor an implicit call runs in, by name and extent: the
|
|
20
|
+
// attribution a whole-file extraction adds (a class name) is not part of it.
|
|
21
|
+
const enclosingKey = enclosing => enclosing ? JSON.stringify([enclosing.name, enclosing.startLine, enclosing.endLine]) : null;
|
|
22
|
+
|
|
23
|
+
// Cache syntax, never a binding verdict. External macro recovery has an
|
|
24
|
+
// additional input and consultation side effects, so keep that path fresh.
|
|
25
|
+
const reusableSyntax = language => !langTraits(language)?.textualIncludes ||
|
|
26
|
+
!require('../languages/c-family').hasExternalMacroContext();
|
|
27
|
+
|
|
28
|
+
function implicitSourceCalls(language, source) {
|
|
29
|
+
const reusable = reusableSyntax(language);
|
|
30
|
+
const saved = reusable ? sourceCalls.get(language, source) : undefined;
|
|
31
|
+
if (saved) return saved;
|
|
32
|
+
const calls = getLanguageAdapter(language).findCallsInCode(source, getParser(language))
|
|
33
|
+
.filter(call => call.constructorSyntax === 'implicit').map(implicitRecord);
|
|
34
|
+
if (reusable) sourceCalls.set(language, source, calls);
|
|
35
|
+
return calls;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The omitted-initializer calls of the class at `base`'s lines (fix #421:
|
|
40
|
+
* the first replay over a large file extracted every call of the file).
|
|
41
|
+
* The adapter reads them from the same trees as a whole-file extraction.
|
|
42
|
+
*/
|
|
43
|
+
const ownerCalls = new SourceMemo({ maxEntries: 128, maxWeight: 8 * 1024 * 1024,
|
|
44
|
+
weigh: byOwner => JSON.stringify([...byOwner.values()]).length * 2 });
|
|
45
|
+
function implicitOwnerSourceCalls(language, source, base) {
|
|
46
|
+
const adapter = getLanguageAdapter(language);
|
|
47
|
+
if (typeof adapter.implicitBaseCallsOf !== 'function' || !Number.isInteger(base?.ownerLine)) return null;
|
|
48
|
+
const reusable = reusableSyntax(language);
|
|
49
|
+
let byOwner = reusable ? ownerCalls.get(language, source) : undefined;
|
|
50
|
+
if (!byOwner) {
|
|
51
|
+
byOwner = new Map();
|
|
52
|
+
if (reusable) ownerCalls.set(language, source, byOwner);
|
|
53
|
+
}
|
|
54
|
+
const key = `${base.ownerLine}:${base.ownerEndLine}`;
|
|
55
|
+
if (!byOwner.has(key)) {
|
|
56
|
+
byOwner.set(key, adapter.implicitBaseCallsOf(source, getParser(language), base.ownerLine, base.ownerEndLine)
|
|
57
|
+
.filter(call => call.constructorSyntax === 'implicit').map(implicitRecord));
|
|
58
|
+
}
|
|
59
|
+
return byOwner.get(key);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function headerSite(call, file) {
|
|
63
|
+
const header = call.implicitBase?.header;
|
|
64
|
+
return header && { file, line: header.line, column: header.column,
|
|
65
|
+
start: header.start, end: header.end };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function captureImplicitBase(index, file, call, owner) {
|
|
69
|
+
const entry = index.files.get(file), language = entry?.language;
|
|
70
|
+
if (!langTraits(language)?.implicitBaseConstruction || call.constructorSyntax !== 'implicit') return null;
|
|
71
|
+
const header = headerSite(call, entry.relativePath);
|
|
72
|
+
if (!header) return null;
|
|
73
|
+
const binding = captureTypeLookup(index, { file, relativePath: entry.relativePath,
|
|
74
|
+
line: header.line, column: header.column, name: call.name,
|
|
75
|
+
qualifier: call.constructorQualifier || null, arity: call.implicitBase.arity,
|
|
76
|
+
position: call.implicitBase.header, construction: true }, owner);
|
|
77
|
+
if (!binding) return null;
|
|
78
|
+
const proof = { language, source: index._readFile(file), site: occurrenceIdentity(entry.relativePath, call),
|
|
79
|
+
name: call.name, qualifier: call.constructorQualifier || null,
|
|
80
|
+
base: call.implicitBase, enclosing: call.enclosingFunction, binding };
|
|
81
|
+
seedImplicitReplay(index, file, entry, call, proof);
|
|
82
|
+
return proof;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// A capture's first replay (fix #421, the #418 rule for other witnesses):
|
|
86
|
+
// the live record was extracted from this same content, so the replay's
|
|
87
|
+
// re-derivation finds exactly it when the file holds one such record. A
|
|
88
|
+
// copied or changed proof is re-derived in full.
|
|
89
|
+
const SEEDED_IMPLICIT = new WeakMap();
|
|
90
|
+
const proofKey = proof => JSON.stringify([proof.language, proof.name, proof.qualifier, proof.site, proof.base,
|
|
91
|
+
enclosingKey(proof.enclosing)]);
|
|
92
|
+
|
|
93
|
+
function seedImplicitReplay(index, file, entry, call, proof) {
|
|
94
|
+
if (index.isProvenanceFactIndex || !require('./source-memo').indexedContent(index, file, entry, proof.source)) return;
|
|
95
|
+
const base = JSON.stringify(call.implicitBase), enclosing = enclosingKey(call.enclosingFunction);
|
|
96
|
+
const same = (require('./callers').getCachedCalls(index, file) || []).filter(other =>
|
|
97
|
+
other.constructorSyntax === 'implicit' && other.name === call.name && other.argCount === 0 &&
|
|
98
|
+
(other.constructorQualifier || null) === proof.qualifier &&
|
|
99
|
+
['line', 'column', 'start', 'end'].every(key => occurrenceIdentity(entry.relativePath, other)[key] === proof.site[key]) &&
|
|
100
|
+
JSON.stringify(other.implicitBase) === base && enclosingKey(other.enclosingFunction) === enclosing);
|
|
101
|
+
if (same.length === 1) SEEDED_IMPLICIT.set(proof, { key: proofKey(proof), source: proof.source,
|
|
102
|
+
header: call.implicitBase.header && { ...call.implicitBase.header } });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Re-derive the omitted initializer from raw source, then replay its header binding. */
|
|
106
|
+
function validateImplicitBase(proof, owner, site) {
|
|
107
|
+
if (!proof || !sameDeclaration(proof.binding?.owner, owner) ||
|
|
108
|
+
typeof proof.source !== 'string' || !langTraits(proof.language)?.implicitBaseConstruction ||
|
|
109
|
+
!site || ['file', 'line', 'column', 'start', 'end'].some(key => proof.site?.[key] !== site[key])) return false;
|
|
110
|
+
try {
|
|
111
|
+
const matching = calls => calls.filter(call => call.name === proof.name &&
|
|
112
|
+
call.qualifier === proof.qualifier && call.argCount === 0 &&
|
|
113
|
+
['line', 'column', 'start', 'end'].every(key => call.site[key] === site[key]) &&
|
|
114
|
+
call.base === JSON.stringify(proof.base) && call.enclosing === enclosingKey(proof.enclosing));
|
|
115
|
+
const seeded = SEEDED_IMPLICIT.get(proof);
|
|
116
|
+
// The class's own calls decide; a call the whole-file extraction
|
|
117
|
+
// places elsewhere (a macro body) is read the whole-file way.
|
|
118
|
+
let matches = seeded && seeded.source === proof.source && seeded.key === proofKey(proof)
|
|
119
|
+
? [{ header: seeded.header }]
|
|
120
|
+
: matching(implicitOwnerSourceCalls(proof.language, proof.source, proof.base) || []);
|
|
121
|
+
if (matches.length !== 1) matches = matching(implicitSourceCalls(proof.language, proof.source));
|
|
122
|
+
const header = matches[0]?.header;
|
|
123
|
+
return matches.length === 1 && validateTypeLookup(proof.binding, owner, header && {
|
|
124
|
+
file: site.file, line: header.line, column: header.column, start: header.start, end: header.end,
|
|
125
|
+
});
|
|
126
|
+
} catch (error) { return require('./provenance-overload').refusedProof(error, false); }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function implicitBaseBinding(index, file, call, candidates) {
|
|
130
|
+
const config = langTraits(index.files.get(file)?.language)?.implicitBaseConstruction;
|
|
131
|
+
if (!config || !call.implicitBase) return null;
|
|
132
|
+
const found = candidates.map(owner => ({ owner, proof: captureImplicitBase(index, file, call, owner) }))
|
|
133
|
+
.filter(entry => entry.proof);
|
|
134
|
+
if (found.length !== 1) return null;
|
|
135
|
+
const { owner, proof } = found[0];
|
|
136
|
+
return { owner, nonClass: !config.baseKinds.includes(owner.type), provenance: {
|
|
137
|
+
rule: 'implicit-base-binding', rules: ['implicit-base-binding'], facts: {
|
|
138
|
+
language: proof.language, site: proof.site, targets: [declarationIdentity(owner)], implicitBaseBinding: proof,
|
|
139
|
+
},
|
|
140
|
+
} };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The indexed source body, distinct from the constructor it initializes. */
|
|
144
|
+
function implicitSourceDefinition(index, file, call) {
|
|
145
|
+
const base = call.implicitBase, scope = base?.scope;
|
|
146
|
+
if (!base || !scope) return null;
|
|
147
|
+
const { ownConstructors } = require('./constructor-calls');
|
|
148
|
+
const owners = (index.symbols.get(base.owner) || []).filter(def => def.file === file &&
|
|
149
|
+
['class', 'struct', 'record'].includes(def.type) && def.startLine === base.ownerLine && def.endLine === base.ownerEndLine &&
|
|
150
|
+
(!Object.hasOwn(scope, 'namespace') || (def.namespace || null) === (scope.namespace || null)) &&
|
|
151
|
+
(def.enclosingType || null) === (scope.enclosingType || null) &&
|
|
152
|
+
!!def.anonymousNamespace === !!scope.anonymousNamespace);
|
|
153
|
+
if (owners.length !== 1) return null;
|
|
154
|
+
if (base.generated) return owners[0];
|
|
155
|
+
const members = ownConstructors(index, owners[0]).filter(def => !def.modifiers?.includes('static') &&
|
|
156
|
+
def.startLine === call.enclosingFunction?.startLine && def.endLine === call.enclosingFunction?.endLine &&
|
|
157
|
+
def.params === base.params);
|
|
158
|
+
return members.length === 1 ? members[0] : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { implicitBaseBinding, validateImplicitBase, implicitSourceDefinition };
|
package/core/provenance-facts.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { langTraits } = require('../languages');
|
|
5
|
-
const { NON_CALLABLE_TYPES } = require('./shared');
|
|
5
|
+
const { NON_CALLABLE_TYPES, isCallableMember } = require('./shared');
|
|
6
6
|
const { declarationIdentity, identityKey, propertyReadMember } = require('./provenance');
|
|
7
7
|
const { captureOverload, overloadMemberGroup } = require('./provenance-overload');
|
|
8
8
|
const { splitParentList } = require('./graph-build');
|
|
9
|
-
const { distinctOwnerDefinitions } = require('./class-identity');
|
|
9
|
+
const { distinctOwnerDefinitions, completeForwardDeclaration } = require('./class-identity');
|
|
10
10
|
|
|
11
11
|
const TYPE_KINDS = new Set(['class', 'struct', 'interface', 'trait', 'record', 'enum', 'type', 'impl']);
|
|
12
12
|
const ownerName = definition => definition.className || (definition.receiver || '').replace(/^\*/, '');
|
|
@@ -36,13 +36,13 @@ function callableMembersOwnedBy(index, name, owner) {
|
|
|
36
36
|
const definitions = index.symbols.get(name) || [];
|
|
37
37
|
const memo = index._opMemo?.('callableMembersByOwner', () => new WeakMap());
|
|
38
38
|
if (!memo) {
|
|
39
|
-
return definitions.filter(d =>
|
|
39
|
+
return definitions.filter(d => isCallableMember(d) && ownerName(d) === owner);
|
|
40
40
|
}
|
|
41
41
|
let byOwner = memo.get(definitions);
|
|
42
42
|
if (!byOwner) {
|
|
43
43
|
byOwner = new Map();
|
|
44
44
|
for (const d of definitions) {
|
|
45
|
-
if (
|
|
45
|
+
if (!isCallableMember(d)) continue;
|
|
46
46
|
const key = ownerName(d);
|
|
47
47
|
let list = byOwner.get(key);
|
|
48
48
|
if (!list) {
|
|
@@ -185,6 +185,65 @@ function ownerCountOf(index, name) {
|
|
|
185
185
|
return count;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Every part of a C# `partial` type (one class across declarations, fix
|
|
190
|
+
* #428): same namespace, enclosing type, name and generic arity, each
|
|
191
|
+
* declared `partial`, in (file, line) order; null for any other type.
|
|
192
|
+
*/
|
|
193
|
+
function partialTypeParts(index, definition) {
|
|
194
|
+
if (!Array.isArray(definition.modifiers) || !definition.modifiers.includes('partial') ||
|
|
195
|
+
index.files.get(definition.file)?.language !== 'csharp') return null;
|
|
196
|
+
const parts = (index.symbols.get(definition.name) || []).filter(d => d.type === definition.type &&
|
|
197
|
+
Array.isArray(d.modifiers) && d.modifiers.includes('partial') &&
|
|
198
|
+
(d.namespace || null) === (definition.namespace || null) &&
|
|
199
|
+
(d.enclosingType || null) === (definition.enclosingType || null) &&
|
|
200
|
+
(d.typeArity || 0) === (definition.typeArity || 0) &&
|
|
201
|
+
index.files.get(d.file)?.language === 'csharp');
|
|
202
|
+
if (parts.length < 2) return null;
|
|
203
|
+
return parts.sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : a.startLine - b.startLine);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Does the Rust name model (core/rust-names.js) bind a bare name at the call
|
|
208
|
+
* to something other than `declaration` (fix #428)? An undecided model
|
|
209
|
+
* answer keeps the witness.
|
|
210
|
+
*/
|
|
211
|
+
function rustNameDisagrees(index, file, call, declaration, ns) {
|
|
212
|
+
if (!langTraits(index.files.get(file)?.language)?.itemScopeResolution || call.isMethod || call.receiver) return false;
|
|
213
|
+
const named = require('./rust-names').resolveRustName(index, file, call.line, call.resolvedName || call.name, ns,
|
|
214
|
+
call.scopeBinding || null);
|
|
215
|
+
if (!named?.defs?.length) return !!named?.external;
|
|
216
|
+
return !named.defs.includes(declaration);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Does a call select its only candidate without reading any fact (fix
|
|
221
|
+
* #428): no type arguments, no spread, no typed argument, exactly the
|
|
222
|
+
* candidate's fixed, non-generic, non-extension parameter list?
|
|
223
|
+
*/
|
|
224
|
+
function soleMemberFits(call, definition) {
|
|
225
|
+
const params = definition.paramsStructured;
|
|
226
|
+
return !call.methodTypeArgs && !call.argSpread && Number.isInteger(call.argCount) &&
|
|
227
|
+
(!Array.isArray(call.argKinds) || call.argKinds.every(kind => kind === 'expr')) &&
|
|
228
|
+
!definition.generics && !definition.isExtensionMethod && Array.isArray(params) &&
|
|
229
|
+
params.length === call.argCount &&
|
|
230
|
+
params.every(param => param && !param.rest && !param.optional && param.default === undefined);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The generic arity of each top-level entry of a base list (`A, B<T, U>` -> [0, 2]). */
|
|
234
|
+
function writtenArities(clause) {
|
|
235
|
+
const out = [];
|
|
236
|
+
let depth = 0;
|
|
237
|
+
let count = 0;
|
|
238
|
+
for (const ch of `${clause},`) {
|
|
239
|
+
if (ch === '<') { if (depth === 0) count = 1; depth++; }
|
|
240
|
+
else if (ch === '>') depth = Math.max(0, depth - 1);
|
|
241
|
+
else if (ch === ',' && depth === 1) count++;
|
|
242
|
+
else if (ch === ',' && depth === 0) { out.push(count); count = 0; }
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
|
|
188
247
|
/** Collect independent facts; a failed lookup never manufactures a negative. */
|
|
189
248
|
function confirmationFacts(index, file, call, targets, options = {}) {
|
|
190
249
|
const entry = index.files.get(file);
|
|
@@ -217,16 +276,55 @@ function confirmationFacts(index, file, call, targets, options = {}) {
|
|
|
217
276
|
};
|
|
218
277
|
}
|
|
219
278
|
if (!methodReceiver) {
|
|
279
|
+
// Explicit construction looks up a type. Its same-named constructor
|
|
280
|
+
// declarations are members, not competing import bindings.
|
|
281
|
+
const constructsType = (call.isConstructor || langTraits(language)?.classesCallableWithoutNew) && targets.length > 0 &&
|
|
282
|
+
targets.every(target => TYPE_KINDS.has(target.type));
|
|
220
283
|
const result = namedDeclaration(index, file, call.resolvedName || call.name,
|
|
221
|
-
d =>
|
|
222
|
-
|
|
284
|
+
d => constructsType ? TYPE_KINDS.has(d.type)
|
|
285
|
+
: !NON_CALLABLE_TYPES.has(d.type) || call.isConstructor, call.line);
|
|
286
|
+
// Rust binds a bare name by its own item and `use` scopes (fix
|
|
287
|
+
// #422): an import chain naming another item than the language's
|
|
288
|
+
// lookup is no witness of it (fix #428).
|
|
289
|
+
if (result?.chain.length && rustNameDisagrees(index, file, call, result.declaration, constructsType ? 'type' : 'value')) {
|
|
290
|
+
facts.lookupGap = 'rust-name-scope';
|
|
291
|
+
} else if (result?.chain.length) facts.importChain = result.chain;
|
|
223
292
|
}
|
|
224
293
|
let type = facts.receiverType;
|
|
294
|
+
let anonymousQualifier = null;
|
|
225
295
|
if (options.sameClass) {
|
|
226
296
|
const enclosing = index.findEnclosingFunction(file, call.line, true, call);
|
|
227
297
|
type = enclosing?.className || enclosing?.receiver?.replace(/^\*/, '');
|
|
228
298
|
facts.receiverTypeSource = 'same-class';
|
|
229
299
|
facts.receiverOrigin = { source: 'same-class', declaration: declarationIdentity(enclosing) };
|
|
300
|
+
// `super.m()` / `super().m()` / `base.M()` look the member up from
|
|
301
|
+
// the enclosing class's bases, never its own members (fix #428): the
|
|
302
|
+
// witness starts there too. In an anonymous class body `super` names
|
|
303
|
+
// the instantiated base type (fix #394), looked up as written.
|
|
304
|
+
const superReceiver = langTraits(language)?.superReceiver;
|
|
305
|
+
if (superReceiver && call.isMethod && call.receiver === superReceiver && !call.constructorSyntax) {
|
|
306
|
+
if (call.receiverSuperType) type = call.receiverSuperType;
|
|
307
|
+
else facts.receiverOrigin.superLookup = true;
|
|
308
|
+
}
|
|
309
|
+
// A bare call in an anonymous class body (`new Base() { .. m() .. }`,
|
|
310
|
+
// fix #401) looks the member up from the instantiated base (fix #428).
|
|
311
|
+
if (!call.receiver && call.anonymousSuperType) {
|
|
312
|
+
const parts = String(call.anonymousSuperType).replace(/<.*$/s, '').trim().split('.');
|
|
313
|
+
type = parts.pop();
|
|
314
|
+
if (parts.length) anonymousQualifier = parts.join('.');
|
|
315
|
+
facts.receiverOrigin.anonymousSuperType = call.anonymousSuperType;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// A static member call is also a reference to the type it is written
|
|
319
|
+
// through (`JsonParser.parseString(..)` for a class query, fix #428):
|
|
320
|
+
// its witness is the type the qualifier names, not a member lookup.
|
|
321
|
+
if (call.isTypeReference && call.receiver && options.resolveType) {
|
|
322
|
+
const written = String(call.receiver).split('.').pop();
|
|
323
|
+
const qualified = String(call.receiver).includes('.') ? String(call.receiver).split('.').slice(0, -1).join('.') : null;
|
|
324
|
+
const named = options.resolveType(written, file, call.line, qualified || call.receiverTypeNamespace || undefined);
|
|
325
|
+
if (named) facts.typeReference = { written: call.receiver, declaration: declarationIdentity(named) };
|
|
326
|
+
else facts.lookupGap = 'type-qualifier-unresolved';
|
|
327
|
+
return facts;
|
|
230
328
|
}
|
|
231
329
|
if (language === 'csharp' && call.receiverCastThis) {
|
|
232
330
|
const enclosing = index.findEnclosingFunction(file, call.line, true, call);
|
|
@@ -239,32 +337,52 @@ function confirmationFacts(index, file, call, targets, options = {}) {
|
|
|
239
337
|
facts.incomplete = ['receiverType'];
|
|
240
338
|
return facts;
|
|
241
339
|
}
|
|
340
|
+
// A C# extension invocation binds a static member of another class
|
|
341
|
+
// through the receiver's `this` parameter: no member lookup on the
|
|
342
|
+
// receiver's type describes it, and this collector has no extension
|
|
343
|
+
// witness yet (a rule not yet validated, fix #428).
|
|
344
|
+
if (options.extensionMethod) {
|
|
345
|
+
facts.lookupGap = 'extension-method';
|
|
346
|
+
return facts;
|
|
347
|
+
}
|
|
242
348
|
if (facts.receiverTypeSource === 'unknown') facts.incomplete = ['receiverTypeSource'];
|
|
243
349
|
const originFile = options.originFile || call.receiverTypeFlowFile || file;
|
|
244
|
-
|
|
350
|
+
// A written generic arity names one of the same-name types where arity is
|
|
351
|
+
// identity (C# `MemberMap` vs `MemberMap<TClass, TMember>`, fix #428).
|
|
352
|
+
const arityFits = (d, arity) => !Number.isInteger(arity) || !langTraits(language)?.genericArityIsIdentity ||
|
|
353
|
+
(d.typeArity || 0) === arity;
|
|
354
|
+
const resolveType = (name, context, atLine, qualifier, arity) => {
|
|
245
355
|
// A qualified annotation cannot bind to a same-named local type.
|
|
246
356
|
// Apply the qualifier only to the receiver's first lookup: aliases
|
|
247
357
|
// and parent annotations are resolved in their own declaration scope.
|
|
248
358
|
if (qualifier && options.resolveType &&
|
|
249
359
|
langTraits(language)?.typeSystem !== 'structural') {
|
|
250
|
-
const definition = options.resolveType(name, context, atLine, qualifier);
|
|
251
|
-
return definition ? {
|
|
360
|
+
const definition = options.resolveType(name, context, atLine, qualifier, arity);
|
|
361
|
+
return definition ? {
|
|
362
|
+
declaration: completeForwardDeclaration(index, definition, file), chain: [] } : null;
|
|
252
363
|
}
|
|
253
364
|
const named = namedDeclaration(index, context, name, d =>
|
|
254
|
-
(TYPE_KINDS.has(d.type) && d.type !== 'impl') ||
|
|
365
|
+
((TYPE_KINDS.has(d.type) && d.type !== 'impl') ||
|
|
255
366
|
(['javascript', 'typescript', 'tsx'].includes(language) &&
|
|
256
367
|
d.type === 'function' && !d.className &&
|
|
257
|
-
['constructor', 'flow'].includes(facts.receiverTypeSource)), atLine);
|
|
258
|
-
if (named) return named
|
|
368
|
+
['constructor', 'flow'].includes(facts.receiverTypeSource))) && arityFits(d, arity), atLine);
|
|
369
|
+
if (named) return { ...named,
|
|
370
|
+
declaration: completeForwardDeclaration(index, named.declaration, file) };
|
|
259
371
|
// Nominal package/type lookup is delegated to the engine's language
|
|
260
372
|
// rules, then pinned to a unique declaration in the resolved scope.
|
|
261
373
|
if (langTraits(language)?.typeSystem !== 'structural' && options.resolveType) {
|
|
262
|
-
const definition = options.resolveType(name, context, atLine);
|
|
263
|
-
if (definition) return {
|
|
374
|
+
const definition = options.resolveType(name, context, atLine, undefined, arity);
|
|
375
|
+
if (definition) return {
|
|
376
|
+
declaration: completeForwardDeclaration(index, definition, file), chain: [] };
|
|
264
377
|
}
|
|
265
378
|
return null;
|
|
266
379
|
};
|
|
267
|
-
|
|
380
|
+
// A Rust path receiver (`crate::m::T::new()`) names the type its module
|
|
381
|
+
// path declares (fix #428).
|
|
382
|
+
const rustPath = langTraits(language)?.itemScopeResolution && facts.receiverTypeSource === 'type-qualified' &&
|
|
383
|
+
String(call.receiver || '').includes('::') && String(call.receiver).split('::').pop() === type
|
|
384
|
+
? String(call.receiver).split('::').slice(0, -1).join('::') : null;
|
|
385
|
+
const qualifier = anonymousQualifier || rustPath || call.receiverTypeNamespace || call.receiverTypeQualifier;
|
|
268
386
|
if (qualifier) facts.receiverTypeQualifier = qualifier;
|
|
269
387
|
// A parser annotation's qualifier belongs to the consuming file. The
|
|
270
388
|
// already-pinned declaration file may be another crate/module, where
|
|
@@ -304,13 +422,54 @@ function confirmationFacts(index, file, call, targets, options = {}) {
|
|
|
304
422
|
if (aliases.length) facts.receiverAliases = aliases;
|
|
305
423
|
facts.receiverResolvedIn = declarationIdentity(resolved.declaration);
|
|
306
424
|
const visited = new Set();
|
|
307
|
-
|
|
425
|
+
// A bare (implicit-this) call whose class hierarchy declares no member
|
|
426
|
+
// of the name looks it up in the lexically enclosing type next (JLS
|
|
427
|
+
// 15.12.1, C# member lookup through enclosing type declarations): the
|
|
428
|
+
// innermost declaration of this file enclosing the type that started
|
|
429
|
+
// the current lexical level (fix #428).
|
|
430
|
+
const lexicalOuterOf = chain => {
|
|
431
|
+
if (call.receiver || facts.receiverTypeSource !== 'same-class' || facts.receiverOrigin?.superLookup) return null;
|
|
432
|
+
let start = 0;
|
|
433
|
+
for (let i = chain.length - 1; i > 0; i--) if (chain[i].enclosingOf) { start = i; break; }
|
|
434
|
+
if (chain.slice(start).some(step => step.members.length > 0)) return null;
|
|
435
|
+
const inner = chain[start].owner;
|
|
436
|
+
if (!inner.enclosingType) return null;
|
|
437
|
+
const segments = String(inner.enclosingType).split('.');
|
|
438
|
+
const name = segments.pop();
|
|
439
|
+
const prefix = segments.length ? segments.join('.') : null;
|
|
440
|
+
const outer = (index.files.get(path.resolve(index.root, inner.file))?.symbols || []).filter(d =>
|
|
441
|
+
TYPE_KINDS.has(d.type) && d.type !== 'impl' && d.name === name && (d.enclosingType || null) === prefix &&
|
|
442
|
+
(d.namespace || null) === (inner.namespace || null) &&
|
|
443
|
+
d.startLine <= inner.startLine && (d.endLine ?? d.startLine) >= inner.endLine)
|
|
444
|
+
.sort((a, b) => b.startLine - a.startLine)[0];
|
|
445
|
+
return outer ? { definition: outer, inner } : null;
|
|
446
|
+
};
|
|
447
|
+
// Choices among same-name members the overload replay cannot state yet
|
|
448
|
+
// (report-only, fix #428): Rust probes inherent impls before trait impls
|
|
449
|
+
// and selects among one type's trait impls by argument type (fixes #384,
|
|
450
|
+
// #399); C++ member pairs that differ in `const` or a ref-qualifier are
|
|
451
|
+
// chosen by the object expression (fix #427).
|
|
452
|
+
const memberChoiceGap = members => {
|
|
453
|
+
if (langTraits(language)?.typeQualifiedCallStyle === 'path' &&
|
|
454
|
+
new Set(members.map(member => (member.traitImpl
|
|
455
|
+
? `trait:${member.traitName || ''}:${member.implSelfRef || ''}` : 'inherent') +
|
|
456
|
+
`:${(member.ownerSelfArgs || []).join(',')}`)).size > 1) return 'impl-selection';
|
|
457
|
+
if (new Set(members.map(member => `${!!member.memberConst}:${member.refQualifier || ''}`)).size > 1) {
|
|
458
|
+
return 'object-qualified-overload';
|
|
459
|
+
}
|
|
460
|
+
return null;
|
|
461
|
+
};
|
|
462
|
+
const walk = (owner, steps, extra = null) => {
|
|
308
463
|
const identity = declarationIdentity(owner);
|
|
309
464
|
const key = identityKey(identity);
|
|
310
465
|
if (visited.has(key) || visited.size >= 16) return null;
|
|
311
466
|
visited.add(key);
|
|
312
467
|
const ownerDefinition = owner.file && TYPE_KINDS.has(owner.type) ? owner : null;
|
|
313
|
-
|
|
468
|
+
// The parts of a C# `partial` type are one class (fix #428): members
|
|
469
|
+
// and bases declared in any part belong to it.
|
|
470
|
+
const partialParts = ownerDefinition ? partialTypeParts(index, ownerDefinition) : null;
|
|
471
|
+
const partFiles = partialParts ? new Set(partialParts.map(part => part.file)) : null;
|
|
472
|
+
let members = callableMembersOwnedBy(index, call.name, owner.name).filter(d => {
|
|
314
473
|
// A member of a same-name class declared in another scope of
|
|
315
474
|
// the file is not this owner's member (fix #389).
|
|
316
475
|
if (ownerDefinition && distinctOwnerDefinitions(index, d, ownerDefinition, true)) return false;
|
|
@@ -318,21 +477,48 @@ function confirmationFacts(index, file, call, targets, options = {}) {
|
|
|
318
477
|
(!facts.receiverCastThis || d.explicitInterface !== facts.receiverCastThis.interfaceType)) return false;
|
|
319
478
|
if ((d.namespace || null) !== (owner.namespace || null)) return false;
|
|
320
479
|
if ((d.enclosingType || null) !== (owner.enclosingType || null)) return false;
|
|
321
|
-
return d.file === owner.file || (language === 'go' &&
|
|
322
|
-
path.dirname(d.file) === path.dirname(owner.file))
|
|
480
|
+
return d.file === owner.file || !!partFiles?.has(d.file) || (language === 'go' &&
|
|
481
|
+
path.dirname(d.file) === path.dirname(owner.file)) ||
|
|
482
|
+
// A Rust impl block anywhere in the crate whose self type
|
|
483
|
+
// names this definition declares its members (fix #428).
|
|
484
|
+
// A member of a same-name type declared in its own file (a
|
|
485
|
+
// function-local struct) belongs to that type.
|
|
486
|
+
(!!ownerDefinition && langTraits(language)?.itemScopeResolution &&
|
|
487
|
+
!require('./class-identity').ownerDefinitionOf(index, d) &&
|
|
488
|
+
!!require('./rust-names').resolveRustName(index, d.file, d.startLine, owner.name, 'type')
|
|
489
|
+
?.defs?.includes(ownerDefinition));
|
|
323
490
|
});
|
|
491
|
+
// A super lookup passes over the enclosing class's own members.
|
|
492
|
+
const superLookup = steps.length === 0 && facts.receiverOrigin?.superLookup === true
|
|
493
|
+
? { skipped: members.map(declarationIdentity) } : null;
|
|
494
|
+
if (superLookup) members = [];
|
|
324
495
|
// Use the AST spelling before the inheritance graph normalizes aliases
|
|
325
496
|
// and qualifiers. A terminal name alone loses its declaring scope.
|
|
326
|
-
const
|
|
327
|
-
d.name ===
|
|
328
|
-
const
|
|
497
|
+
const rawOf = part => index.files.get(part.file)?.symbols.find(d =>
|
|
498
|
+
d.name === part.name && d.startLine === part.startLine && d.type === part.type) || part;
|
|
499
|
+
const rawOwner = rawOf(owner);
|
|
500
|
+
const written = [];
|
|
501
|
+
for (const part of partialParts || [owner]) {
|
|
502
|
+
const raw = part === owner ? rawOwner : rawOf(part);
|
|
503
|
+
const arities = raw.extends ? writtenArities(raw.extends) : [];
|
|
504
|
+
(raw.extends ? splitParentList(raw.extends) : []).forEach((name, i) => written.push({ name, part, arity: arities[i] }));
|
|
505
|
+
}
|
|
329
506
|
const derefTarget = language === 'rust' ? rawOwner.derefTarget : null;
|
|
330
|
-
const
|
|
331
|
-
.map(
|
|
507
|
+
const resolvedParents = [...written, ...(derefTarget ? [{ name: derefTarget, part: owner }] : [])]
|
|
508
|
+
.map(entry => resolveType(entry.name, entry.part.file, entry.part.startLine, undefined, entry.arity));
|
|
509
|
+
const resolvedCount = resolvedParents.filter(Boolean).length;
|
|
510
|
+
// Two parts may name one base: it is one parent.
|
|
511
|
+
const parentKeys = new Set();
|
|
512
|
+
const parents = resolvedParents.filter(Boolean).map(r => r.declaration).filter(parent => {
|
|
513
|
+
const parentKey = identityKey(declarationIdentity(parent));
|
|
514
|
+
if (parentKeys.has(parentKey)) return false;
|
|
515
|
+
parentKeys.add(parentKey);
|
|
516
|
+
return true;
|
|
517
|
+
});
|
|
332
518
|
// The member may be inherited from an external declaration. This
|
|
333
519
|
// collector has no platform-member lookup; report the coverage gap
|
|
334
520
|
// separately from an ambiguous or inconsistent project lookup.
|
|
335
|
-
if (!members.length &&
|
|
521
|
+
if (!members.length && resolvedCount < written.length) {
|
|
336
522
|
facts.lookupUnsupported = 'external-parent-member';
|
|
337
523
|
return null;
|
|
338
524
|
}
|
|
@@ -354,10 +540,24 @@ function confirmationFacts(index, file, call, targets, options = {}) {
|
|
|
354
540
|
} catch {
|
|
355
541
|
choice = null;
|
|
356
542
|
}
|
|
543
|
+
// A base member brought in is chosen by a lookup this
|
|
544
|
+
// witness cannot state yet: report-only (fix #428).
|
|
545
|
+
if (choice?.match && baseMembers.includes(choice.match)) {
|
|
546
|
+
facts.lookupGap = 'using-declaration-member';
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
357
549
|
if (!choice?.match || members.length !== 1 || choice.match !== members[0]) return null;
|
|
358
550
|
}
|
|
359
551
|
}
|
|
552
|
+
const delegation = call.constructorSyntax === 'delegation' && call.receiver === 'super' &&
|
|
553
|
+
steps.length === 0 && facts.receiverOrigin?.source === 'same-class'
|
|
554
|
+
? { enclosing: facts.receiverOrigin.declaration, members: members.map(declarationIdentity) } : null;
|
|
555
|
+
if (delegation) members = [];
|
|
360
556
|
const step = { owner: identity, members: members.map(declarationIdentity), parents: parents.map(declarationIdentity),
|
|
557
|
+
...(delegation && { constructorDelegation: delegation }),
|
|
558
|
+
...(superLookup && { superLookup }),
|
|
559
|
+
...(extra?.enclosingOf && { enclosingOf: extra.enclosingOf }),
|
|
560
|
+
...(partialParts && { partialParts: partialParts.map(declarationIdentity) }),
|
|
361
561
|
...(derefTarget && { derefTarget }) };
|
|
362
562
|
const getter = facts.valueReference && propertyReadMember(step.members);
|
|
363
563
|
if (getter) return { receiver: declarationIdentity(resolved.declaration),
|
|
@@ -368,7 +568,21 @@ function confirmationFacts(index, file, call, targets, options = {}) {
|
|
|
368
568
|
if (parents.length === 1) return walk(parents[0], chain);
|
|
369
569
|
if (parents.length > 1) return null;
|
|
370
570
|
const group = overloadMemberGroup(chain);
|
|
371
|
-
if (!group?.length)
|
|
571
|
+
if (!group?.length) {
|
|
572
|
+
const outer = lexicalOuterOf(chain);
|
|
573
|
+
return outer ? walk(outer.definition, chain, { enclosingOf: outer.inner }) : null;
|
|
574
|
+
}
|
|
575
|
+
// One member of the name in the whole chain, a call of exactly its
|
|
576
|
+
// fixed parameter count with no typed argument and no type
|
|
577
|
+
// arguments: overload selection reads nothing (fix #428), the
|
|
578
|
+
// lookup ends at the declaring step.
|
|
579
|
+
if (group.length === 1 && soleMemberFits(call, group[0])) {
|
|
580
|
+
const declaring = chain.findIndex(chainStep => chainStep.members.length > 0);
|
|
581
|
+
if (declaring >= 0 && chain[declaring].members.length === 1) {
|
|
582
|
+
return { receiver: declarationIdentity(resolved.declaration), steps: chain.slice(0, declaring + 1),
|
|
583
|
+
selected: declarationIdentity(group[0]) };
|
|
584
|
+
}
|
|
585
|
+
}
|
|
372
586
|
const overload = captureOverload(index, call, group, language, options.selectOverload);
|
|
373
587
|
if (!overload) return null;
|
|
374
588
|
return { receiver: declarationIdentity(resolved.declaration), steps: chain,
|
|
@@ -406,6 +620,8 @@ function confirmationFacts(index, file, call, targets, options = {}) {
|
|
|
406
620
|
receiver: declarationIdentity(resolved.declaration),
|
|
407
621
|
steps: [...steps, { ...step, overload }], selected: overload.selected,
|
|
408
622
|
};
|
|
623
|
+
const gap = memberChoiceGap(members);
|
|
624
|
+
if (gap) facts.lookupGap = gap;
|
|
409
625
|
// Every member the lookup found is known but the arguments cannot
|
|
410
626
|
// choose one (fix #396). Name lookup stops at the declaring class,
|
|
411
627
|
// so the callee is one of its members of that name: the witness
|
|
@@ -419,6 +635,20 @@ function confirmationFacts(index, file, call, targets, options = {}) {
|
|
|
419
635
|
selected: declarationIdentity(members[0]), memberSet: true,
|
|
420
636
|
};
|
|
421
637
|
}
|
|
638
|
+
if (gap) return null;
|
|
639
|
+
}
|
|
640
|
+
const gap = members.length > 1 ? memberChoiceGap(members) : null;
|
|
641
|
+
if (gap) {
|
|
642
|
+
facts.lookupGap = gap;
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
// A Rust type also takes the methods of traits it implements, a
|
|
646
|
+
// trait's provided methods included: no member of the name among its
|
|
647
|
+
// impls is a lookup this witness cannot finish (report-only, fix #428).
|
|
648
|
+
if (!members.length && !parents.length && langTraits(language)?.itemScopeResolution && !superLookup &&
|
|
649
|
+
['struct', 'enum', 'union'].includes(owner.type)) {
|
|
650
|
+
facts.lookupGap = 'trait-method-probe';
|
|
651
|
+
return null;
|
|
422
652
|
}
|
|
423
653
|
if (members.length || parents.length !== 1) return null;
|
|
424
654
|
return walk(parents[0], [...steps, step]);
|
|
@@ -428,4 +658,4 @@ function confirmationFacts(index, file, call, targets, options = {}) {
|
|
|
428
658
|
return facts;
|
|
429
659
|
}
|
|
430
660
|
|
|
431
|
-
module.exports = { confirmationFacts, namedDeclaration, occurrenceIdentity, definitionsInFile, definitionAt };
|
|
661
|
+
module.exports = { confirmationFacts, namedDeclaration, occurrenceIdentity, definitionsInFile, definitionAt, partialTypeParts };
|