ucn 5.4.2 → 5.5.1
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 +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +27 -16
- package/cli/index.js +45 -14
- package/core/accessors.js +118 -3
- package/core/account.js +62 -11
- package/core/analysis.js +517 -266
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +529 -31
- package/core/callers.js +12228 -921
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +7 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +305 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +472 -66
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +3026 -0
- package/core/rust-modules.js +203 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1185 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2265 -112
- package/languages/utils.js +298 -4
- package/mcp/server.js +22 -8
- package/package.json +1 -1
|
@@ -0,0 +1,1028 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/class-identity.js - Class DEFINITION identity for same-class resolution.
|
|
3
|
+
*
|
|
4
|
+
* `self.x()` / `this.x()` / `Self::x()` / `base.x()` bind through the
|
|
5
|
+
* enclosing class DEFINITION, not its name: two unrelated classes named
|
|
6
|
+
* `Chunks` in different modules, packages, namespaces or crates never share
|
|
7
|
+
* members. Identity is modelled as the owning class definition:
|
|
8
|
+
*
|
|
9
|
+
* classKey = file | namespace | enclosing lexical scope | name
|
|
10
|
+
*
|
|
11
|
+
* Same-scope redeclarations in one file (TypeScript class + interface
|
|
12
|
+
* merging, Python if/else class variants) share a key, and so do the parts
|
|
13
|
+
* of a C# `partial` type across files; function-local and nested same-name
|
|
14
|
+
* classes do not.
|
|
15
|
+
*
|
|
16
|
+
* A member's owner resolves by lexical containment in its own file first,
|
|
17
|
+
* then (members declared outside the type body: Rust `impl`, C++ out-of-line
|
|
18
|
+
* definitions, JS prototype assignments) through the language's own
|
|
19
|
+
* resolution: same file, same package directory, direct imports, same or
|
|
20
|
+
* enclosing namespace, then a bounded transitive import walk. An owner that
|
|
21
|
+
* cannot be resolved to exactly one definition has `key: null`; consumers
|
|
22
|
+
* must treat it as unknown (route unverified), never as evidence for or
|
|
23
|
+
* against a candidate.
|
|
24
|
+
*
|
|
25
|
+
* When only one class definition carries a name, name identity IS
|
|
26
|
+
* definition identity and the name key `name:<Name>` is used without any
|
|
27
|
+
* resolution work.
|
|
28
|
+
*
|
|
29
|
+
* Every derivation is memoized on the index and reset at every build/load.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const { langTraits } = require('../languages');
|
|
34
|
+
const { splitParentList } = require('./graph-build');
|
|
35
|
+
const { genericArityOf } = require('../languages/utils');
|
|
36
|
+
|
|
37
|
+
const OWNER_CLASS_KINDS = new Set([
|
|
38
|
+
'class', 'struct', 'interface', 'trait', 'enum', 'record', 'union',
|
|
39
|
+
]);
|
|
40
|
+
const TRANSITIVE_IMPORT_DEPTH = 4;
|
|
41
|
+
const TRANSITIVE_IMPORT_FILE_CAP = 2000;
|
|
42
|
+
|
|
43
|
+
function _state(index) {
|
|
44
|
+
let state = index._classIdentity;
|
|
45
|
+
if (!state) {
|
|
46
|
+
state = {
|
|
47
|
+
named: new Map(),
|
|
48
|
+
classKeys: new WeakMap(),
|
|
49
|
+
owners: new WeakMap(),
|
|
50
|
+
resolved: new Map(),
|
|
51
|
+
parents: new Map(),
|
|
52
|
+
modules: new Map(),
|
|
53
|
+
// fix #365: target-independent derivations repeated for every
|
|
54
|
+
// same-name definition a caller query pins (stats --hot / repo).
|
|
55
|
+
declaring: new WeakMap(), // definitions array -> Map<refKey|mode, result>
|
|
56
|
+
descendants: new Map(), // class key -> descendant key Set (default cap)
|
|
57
|
+
// fix #389: certain owner definitions, same-file namesake counts,
|
|
58
|
+
// names with function-local class definitions.
|
|
59
|
+
ownerDefs: new WeakMap(),
|
|
60
|
+
namesakes: new Map(),
|
|
61
|
+
localNames: new Map(),
|
|
62
|
+
// fix #390: class name -> Set of files declaring it without a
|
|
63
|
+
// possible base elsewhere (see fileDeclaresClass).
|
|
64
|
+
declaringFiles: new Map(),
|
|
65
|
+
};
|
|
66
|
+
index._classIdentity = state;
|
|
67
|
+
}
|
|
68
|
+
return state;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Reset every memoized identity derivation (called at build and cache load). */
|
|
72
|
+
function resetClassIdentity(index) {
|
|
73
|
+
index._classIdentity = null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function ownerNameOf(member) {
|
|
77
|
+
if (!member) return null;
|
|
78
|
+
if (member.className) return member.className;
|
|
79
|
+
if (member.receiver) return String(member.receiver).replace(/^\*/, '') || null;
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Innermost enclosing lexical scope of a class definition inside its own
|
|
85
|
+
* file (another def whose range strictly contains it), 0 at module level.
|
|
86
|
+
*/
|
|
87
|
+
function _enclosingScopeStart(index, def) {
|
|
88
|
+
const fileEntry = index.files.get(def.file);
|
|
89
|
+
const symbols = fileEntry?.symbols;
|
|
90
|
+
if (!Array.isArray(symbols)) return 0;
|
|
91
|
+
let best = null;
|
|
92
|
+
for (const s of symbols) {
|
|
93
|
+
if (s === def || s.startLine == null || s.endLine == null) continue;
|
|
94
|
+
if (s.startLine > def.startLine) continue;
|
|
95
|
+
if (s.endLine < def.endLine) continue;
|
|
96
|
+
if (s.startLine === def.startLine && s.endLine === def.endLine) continue;
|
|
97
|
+
if (!best || (s.endLine - s.startLine) < (best.endLine - best.startLine)) best = s;
|
|
98
|
+
}
|
|
99
|
+
return best ? best.startLine : 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Identity key of one class definition. */
|
|
103
|
+
function classKeyOf(index, def) {
|
|
104
|
+
const state = _state(index);
|
|
105
|
+
let key = state.classKeys.get(def);
|
|
106
|
+
if (key === undefined) {
|
|
107
|
+
// A `partial` type (C#) is ONE class declared across files: its
|
|
108
|
+
// identity is the namespace + enclosing type + name, never the file.
|
|
109
|
+
// Generic arity is part of the identity (fix #380): only parts with
|
|
110
|
+
// the same type-parameter count merge (`partial class P<T>` never
|
|
111
|
+
// joins `partial class P`).
|
|
112
|
+
const arity = def.typeArity ? `|${def.typeArity}` : '';
|
|
113
|
+
key = Array.isArray(def.modifiers) && def.modifiers.includes('partial')
|
|
114
|
+
? `partial|${def.namespace || ''}|${def.enclosingType || ''}|${def.name}${arity}`
|
|
115
|
+
: `${def.file}|${def.namespace || ''}|${_enclosingScopeStart(index, def)}|${def.name}${arity}`;
|
|
116
|
+
state.classKeys.set(def, key);
|
|
117
|
+
}
|
|
118
|
+
return key;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Class definitions carrying `name`: { entries: [{def, key}], unique }.
|
|
123
|
+
* `unique` is true when all of them share one identity (or there are none).
|
|
124
|
+
*/
|
|
125
|
+
function classDefsNamed(index, name) {
|
|
126
|
+
const state = _state(index);
|
|
127
|
+
let info = state.named.get(name);
|
|
128
|
+
if (info) return info;
|
|
129
|
+
const defs = (index.symbols.get(name) || [])
|
|
130
|
+
.filter(d => d && d.name === name && OWNER_CLASS_KINDS.has(d.type) && d.file);
|
|
131
|
+
// Keys are only needed to tell several same-name definitions apart.
|
|
132
|
+
let entries = defs.length <= 1
|
|
133
|
+
? defs.map(def => ({ def, key: `name:${name}` }))
|
|
134
|
+
: defs.map(def => ({ def, key: classKeyOf(index, def) }));
|
|
135
|
+
const unique = new Set(entries.map(e => e.key)).size <= 1;
|
|
136
|
+
// One identity (a single def, or the parts of one partial/merged type):
|
|
137
|
+
// every consumer uses the name key, so comparisons stay consistent.
|
|
138
|
+
if (unique) entries = entries.map(e => ({ def: e.def, key: `name:${name}` }));
|
|
139
|
+
const hasScoped = defs.some(_functionLocal);
|
|
140
|
+
info = { entries, unique, hasScoped };
|
|
141
|
+
state.named.set(name, info);
|
|
142
|
+
return info;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function _nameRef(name, info) {
|
|
146
|
+
return { name, key: `name:${name}`, def: info.entries[0]?.def || null };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function _single(entries) {
|
|
150
|
+
if (entries.length === 0) return null;
|
|
151
|
+
const keys = new Set(entries.map(e => e.key));
|
|
152
|
+
if (keys.size !== 1) return null;
|
|
153
|
+
// Prefer a class-kind def as the representative (TS class + interface).
|
|
154
|
+
const rep = entries.find(e => e.def.type === 'class') || entries[0];
|
|
155
|
+
return rep;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function _contextNamespace(index, contextFile, contextNamespace) {
|
|
159
|
+
if (contextNamespace != null) return contextNamespace;
|
|
160
|
+
const symbols = index.files.get(contextFile)?.symbols;
|
|
161
|
+
if (!Array.isArray(symbols)) return null;
|
|
162
|
+
const withNs = symbols.find(s => s.namespace);
|
|
163
|
+
return withNs ? withNs.namespace : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function _namespaceVisible(candidateNs, contextNs, separator) {
|
|
167
|
+
if (!candidateNs) return false;
|
|
168
|
+
if (candidateNs === contextNs) return true;
|
|
169
|
+
return !!contextNs && contextNs.startsWith(candidateNs + separator);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function _transitiveImportMatch(index, contextFile, entries) {
|
|
173
|
+
const candidateFiles = new Set(entries.map(e => e.def.file));
|
|
174
|
+
const reached = [];
|
|
175
|
+
const seen = new Set([contextFile]);
|
|
176
|
+
let frontier = [...(index.importGraph.get(contextFile) || [])];
|
|
177
|
+
for (let depth = 1; depth <= TRANSITIVE_IMPORT_DEPTH && frontier.length > 0; depth++) {
|
|
178
|
+
const next = [];
|
|
179
|
+
for (const file of frontier) {
|
|
180
|
+
if (seen.has(file)) continue;
|
|
181
|
+
seen.add(file);
|
|
182
|
+
if (seen.size > TRANSITIVE_IMPORT_FILE_CAP) return null;
|
|
183
|
+
if (candidateFiles.has(file)) reached.push(file);
|
|
184
|
+
for (const n of index.importGraph.get(file) || []) {
|
|
185
|
+
if (!seen.has(n)) next.push(n);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Nearest reach wins: a deeper match never overrides a closer one.
|
|
189
|
+
if (reached.length > 0) break;
|
|
190
|
+
frontier = next;
|
|
191
|
+
}
|
|
192
|
+
if (reached.length === 0) return null;
|
|
193
|
+
const files = new Set(reached);
|
|
194
|
+
return _single(entries.filter(e => files.has(e.def.file)));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Resolve a class NAME as seen from `contextFile` to one class definition.
|
|
199
|
+
* @param {object} [opts] - { excludeKey, namespace }: excludeKey drops one
|
|
200
|
+
* definition (a class is never its own parent: `class Foo(base.Foo)`).
|
|
201
|
+
* @returns {{name, key, def}} - key null when the name has several
|
|
202
|
+
* definitions and the language's resolution cannot pick exactly one.
|
|
203
|
+
*/
|
|
204
|
+
function resolveClassRef(index, name, contextFile, opts = {}) {
|
|
205
|
+
const info = classDefsNamed(index, name);
|
|
206
|
+
// Function-local classes (fix #381) are visible only inside their
|
|
207
|
+
// declaring function: never from another file, and from their own file
|
|
208
|
+
// only at a line inside that scope (when the caller knows the line).
|
|
209
|
+
if (contextFile && info.hasScoped) {
|
|
210
|
+
return _resolveScopedClassRef(index, name, contextFile, opts, info);
|
|
211
|
+
}
|
|
212
|
+
const excludeKey = opts.excludeKey || null;
|
|
213
|
+
// A written generic arity (fix #380, trait genericArityIsIdentity) names
|
|
214
|
+
// only the same-arity type: `: Outcome<T>` never resolves to `Outcome`.
|
|
215
|
+
const arity = Number.isInteger(opts.arity) && info.entries.some(e =>
|
|
216
|
+
(e.def.typeArity || 0) !== opts.arity) ? opts.arity : null;
|
|
217
|
+
if (info.unique && arity == null && !(excludeKey && info.entries.some(e => e.key === excludeKey))) {
|
|
218
|
+
return _nameRef(name, info);
|
|
219
|
+
}
|
|
220
|
+
const state = _state(index);
|
|
221
|
+
const cacheKey = `${name}\0${contextFile || ''}\0${excludeKey || ''}\0${opts.namespace || ''}\0${arity ?? ''}`;
|
|
222
|
+
if (state.resolved.has(cacheKey)) return state.resolved.get(cacheKey);
|
|
223
|
+
let entries = excludeKey ? info.entries.filter(e => e.key !== excludeKey) : info.entries;
|
|
224
|
+
if (arity != null) entries = entries.filter(e => (e.def.typeArity || 0) === arity);
|
|
225
|
+
// No remaining project definition: the name denotes an external type
|
|
226
|
+
// (never a same-class match); several unresolvable ones: unknown.
|
|
227
|
+
const hit = _resolveAmong(index, entries, contextFile, opts.namespace);
|
|
228
|
+
const ref = hit ? { name, key: hit.key, def: hit.def }
|
|
229
|
+
: { name, key: null, def: null, external: entries.length === 0 };
|
|
230
|
+
state.resolved.set(cacheKey, ref);
|
|
231
|
+
return ref;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* A class declared in a function body (Python, JS/TS, Go): lexically scoped
|
|
236
|
+
* and without an enclosing TYPE (C++ nested types carry their class range as
|
|
237
|
+
* scope but are reachable through the enclosing type's name).
|
|
238
|
+
*/
|
|
239
|
+
function _functionLocal(def) {
|
|
240
|
+
return def.lexicalScopeStartLine != null && !def.enclosingType;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function _resolveScopedClassRef(index, name, contextFile, opts, info) {
|
|
244
|
+
const line = Number.isInteger(opts.line) ? opts.line : null;
|
|
245
|
+
const state = _state(index);
|
|
246
|
+
const cacheKey = `scoped\0${name}\0${contextFile}\0${opts.excludeKey || ''}\0${opts.namespace || ''}\0${opts.arity ?? ''}\0${line ?? ''}`;
|
|
247
|
+
if (state.resolved.has(cacheKey)) return state.resolved.get(cacheKey);
|
|
248
|
+
const inScope = e => line != null &&
|
|
249
|
+
line >= e.def.lexicalScopeStartLine && line <= e.def.lexicalScopeEndLine;
|
|
250
|
+
let entries = info.entries.filter(e => e.key !== opts.excludeKey);
|
|
251
|
+
if (Number.isInteger(opts.arity)) {
|
|
252
|
+
const sameArity = entries.filter(e => (e.def.typeArity || 0) === opts.arity);
|
|
253
|
+
if (sameArity.length > 0) entries = sameArity;
|
|
254
|
+
}
|
|
255
|
+
let ref;
|
|
256
|
+
const enclosing = entries.filter(e => _functionLocal(e.def) &&
|
|
257
|
+
e.def.file === contextFile && inScope(e));
|
|
258
|
+
if (enclosing.length > 0) {
|
|
259
|
+
// The innermost enclosing scope's declaration shadows the rest.
|
|
260
|
+
const width = e => e.def.lexicalScopeEndLine - e.def.lexicalScopeStartLine;
|
|
261
|
+
const narrowest = Math.min(...enclosing.map(width));
|
|
262
|
+
const hit = _single(enclosing.filter(e => width(e) === narrowest));
|
|
263
|
+
ref = hit ? { name, key: hit.key, def: hit.def } : { name, key: null, def: null };
|
|
264
|
+
} else {
|
|
265
|
+
// Without a line, a same-file local class keeps its legacy
|
|
266
|
+
// visibility (the file's own resolution decides); another file's
|
|
267
|
+
// local class is never visible.
|
|
268
|
+
const visible = entries.filter(e => !_functionLocal(e.def) ||
|
|
269
|
+
(line == null && e.def.file === contextFile));
|
|
270
|
+
const hit = _resolveAmong(index, visible, contextFile, opts.namespace);
|
|
271
|
+
ref = hit ? { name, key: hit.key, def: hit.def }
|
|
272
|
+
: { name, key: null, def: null, external: visible.length === 0 };
|
|
273
|
+
}
|
|
274
|
+
state.resolved.set(cacheKey, ref);
|
|
275
|
+
return ref;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function _resolveAmong(index, entries, contextFile, namespace) {
|
|
279
|
+
if (entries.length === 0) return null;
|
|
280
|
+
const only = _single(entries);
|
|
281
|
+
if (only) return only;
|
|
282
|
+
if (!contextFile) return null;
|
|
283
|
+
// 1. Same file (the module's own scope; nested/function-local variants
|
|
284
|
+
// are separate keys and stay ambiguous).
|
|
285
|
+
const sameFile = entries.filter(e => e.def.file === contextFile);
|
|
286
|
+
if (sameFile.length > 0) {
|
|
287
|
+
const hit = _single(sameFile) ||
|
|
288
|
+
_single(sameFile.filter(e => _enclosingScopeStart(index, e.def) === 0));
|
|
289
|
+
return hit || null;
|
|
290
|
+
}
|
|
291
|
+
const language = index.files.get(contextFile)?.language;
|
|
292
|
+
const traits = langTraits(language);
|
|
293
|
+
// 2. Directory package scope (Go): every file of the package sees it.
|
|
294
|
+
if (traits?.packageScope === 'directory') {
|
|
295
|
+
const dir = path.dirname(contextFile);
|
|
296
|
+
const samePkg = entries.filter(e => path.dirname(e.def.file) === dir);
|
|
297
|
+
if (samePkg.length > 0) return _single(samePkg);
|
|
298
|
+
}
|
|
299
|
+
// 3. Direct imports.
|
|
300
|
+
const imports = index.importGraph.get(contextFile);
|
|
301
|
+
if (imports && imports.size > 0) {
|
|
302
|
+
const imported = entries.filter(e => imports.has(e.def.file));
|
|
303
|
+
if (imported.length > 1) {
|
|
304
|
+
// fix #367i: several imported files define the name - keep the
|
|
305
|
+
// one whose namespace the file actually imports (C# `using N1;`,
|
|
306
|
+
// Java `import a.b.Base;`), not a sibling type reached through
|
|
307
|
+
// another directive into the same file set (`using static
|
|
308
|
+
// N2.Util` brings Util's members, never N2.Base).
|
|
309
|
+
const modules = Object.keys(index.files.get(contextFile)?.moduleResolved || {});
|
|
310
|
+
if (modules.length > 0) {
|
|
311
|
+
const named = imported.filter(e => e.def.namespace &&
|
|
312
|
+
modules.some(mod => mod === e.def.namespace ||
|
|
313
|
+
mod === `${e.def.namespace}.${e.def.name}`));
|
|
314
|
+
const hit = _single(named);
|
|
315
|
+
if (hit) return hit;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (imported.length > 0) return _single(imported);
|
|
319
|
+
}
|
|
320
|
+
// 4. Same or enclosing namespace/package (Java packages, C#/C++
|
|
321
|
+
// namespaces), nearest first.
|
|
322
|
+
const ns = _contextNamespace(index, contextFile, namespace);
|
|
323
|
+
if (ns) {
|
|
324
|
+
const separator = ns.includes('::') ? '::' : '.';
|
|
325
|
+
const visible = entries.filter(e =>
|
|
326
|
+
_namespaceVisible(e.def.namespace, ns, separator));
|
|
327
|
+
if (visible.length > 0) {
|
|
328
|
+
const longest = Math.max(...visible.map(e => e.def.namespace.length));
|
|
329
|
+
return _single(visible.filter(e => e.def.namespace.length === longest));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// 5. Re-exports: the nearest transitive import reach, when unique.
|
|
333
|
+
return _transitiveImportMatch(index, contextFile, entries);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Owning class definition of a member (method/field/constructor def, or a
|
|
338
|
+
* caller symbol with className/receiver).
|
|
339
|
+
* @returns {{name, key, def}|null} - null when the member has no owner.
|
|
340
|
+
*/
|
|
341
|
+
function ownerRefOf(index, member) {
|
|
342
|
+
const name = ownerNameOf(member);
|
|
343
|
+
if (!name) return null;
|
|
344
|
+
const state = _state(index);
|
|
345
|
+
const cached = state.owners.get(member);
|
|
346
|
+
if (cached !== undefined) return cached;
|
|
347
|
+
const info = classDefsNamed(index, name);
|
|
348
|
+
let ref;
|
|
349
|
+
if (info.unique) {
|
|
350
|
+
ref = _nameRef(name, info);
|
|
351
|
+
} else {
|
|
352
|
+
// A Rust impl is outside the type's body. Its lexical scope still
|
|
353
|
+
// fixes its owner: an impl of a block-local type must not borrow the
|
|
354
|
+
// module-level namesake's member identity.
|
|
355
|
+
const scopedOwner = info.hasScoped && ownerDefinitionOf(index, member);
|
|
356
|
+
const scopedEntry = scopedOwner && info.entries.find(e => e.def === scopedOwner);
|
|
357
|
+
// Lexical containment: the member is declared inside the type body.
|
|
358
|
+
let best = scopedEntry || null;
|
|
359
|
+
for (const e of info.entries) {
|
|
360
|
+
if (scopedEntry) break;
|
|
361
|
+
const d = e.def;
|
|
362
|
+
if (d.file !== member.file) continue;
|
|
363
|
+
if (d.startLine > member.startLine || d.endLine < member.startLine) continue;
|
|
364
|
+
if (!best || (d.endLine - d.startLine) < (best.def.endLine - best.def.startLine)) best = e;
|
|
365
|
+
}
|
|
366
|
+
if (best) {
|
|
367
|
+
ref = { name, key: best.key, def: best.def };
|
|
368
|
+
} else {
|
|
369
|
+
ref = resolveClassRef(index, name, member.file,
|
|
370
|
+
{ namespace: member.namespace || null });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
state.owners.set(member, ref);
|
|
374
|
+
return ref;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The class definition a member is declared in, when the language's own
|
|
379
|
+
* scoping makes that certain (fix #389): the innermost same-name type body
|
|
380
|
+
* in the member's file that contains it; for a member of a Rust `impl`
|
|
381
|
+
* block, the type of that name the impl block itself sees (a block-local
|
|
382
|
+
* struct when the impl sits in its block, else the file's single
|
|
383
|
+
* module-level declaration). null when the owner is declared elsewhere or
|
|
384
|
+
* cannot be decided.
|
|
385
|
+
*/
|
|
386
|
+
function ownerDefinitionOf(index, member) {
|
|
387
|
+
const name = ownerNameOf(member);
|
|
388
|
+
if (!name || !member?.file || member.startLine == null) return null;
|
|
389
|
+
const state = _state(index);
|
|
390
|
+
if (state.ownerDefs.has(member)) return state.ownerDefs.get(member);
|
|
391
|
+
const defs = (index.symbols.get(name) || []).filter(d => d && d !== member &&
|
|
392
|
+
d.name === name && OWNER_CLASS_KINDS.has(d.type) && d.file === member.file);
|
|
393
|
+
const span = d => d.endLine - d.startLine;
|
|
394
|
+
const containing = (list, line, endLine = line) => list.filter(d =>
|
|
395
|
+
d.startLine <= line && d.endLine >= endLine);
|
|
396
|
+
let result = null;
|
|
397
|
+
const around = containing(defs, member.startLine, member.endLine ?? member.startLine);
|
|
398
|
+
if (around.length > 0) {
|
|
399
|
+
const narrowest = Math.min(...around.map(span));
|
|
400
|
+
const best = around.filter(d => span(d) === narrowest);
|
|
401
|
+
result = best.length === 1 ? best[0] : null;
|
|
402
|
+
} else if (defs.length > 0) {
|
|
403
|
+
const symbols = index.files.get(member.file)?.symbols || [];
|
|
404
|
+
const impls = containing(symbols.filter(d => d.type === 'impl' &&
|
|
405
|
+
String(d.typeName || d.name || '').replace(/<.*$/s, '').trim() === name),
|
|
406
|
+
member.startLine, member.endLine ?? member.startLine);
|
|
407
|
+
if (impls.length === 1) {
|
|
408
|
+
const impl = impls[0];
|
|
409
|
+
const scoped = defs.filter(d => d.lexicalScopeStartLine != null &&
|
|
410
|
+
impl.startLine >= d.lexicalScopeStartLine && impl.startLine <= d.lexicalScopeEndLine);
|
|
411
|
+
if (scoped.length > 0) {
|
|
412
|
+
const narrowest = Math.min(...scoped.map(d => d.lexicalScopeEndLine - d.lexicalScopeStartLine));
|
|
413
|
+
const best = scoped.filter(d => d.lexicalScopeEndLine - d.lexicalScopeStartLine === narrowest);
|
|
414
|
+
result = best.length === 1 ? best[0] : null;
|
|
415
|
+
} else {
|
|
416
|
+
const moduleLevel = defs.filter(d => d.lexicalScopeStartLine == null);
|
|
417
|
+
result = moduleLevel.length === 1 ? moduleLevel[0] : null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
state.ownerDefs.set(member, result);
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Does any class definition of `name` live in a function body (fix #389)?
|
|
427
|
+
* A cheap memoized gate for the function-local scoping rules.
|
|
428
|
+
*/
|
|
429
|
+
function hasFunctionLocalClass(index, name) {
|
|
430
|
+
const state = _state(index);
|
|
431
|
+
let has = state.localNames.get(name);
|
|
432
|
+
if (has === undefined) {
|
|
433
|
+
has = (index.symbols.get(name) || []).some(d => d && OWNER_CLASS_KINDS.has(d.type) && _functionLocal(d));
|
|
434
|
+
state.localNames.set(name, has);
|
|
435
|
+
}
|
|
436
|
+
return has;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Does `file` itself declare a class named `name` whose bases can only be
|
|
441
|
+
* written there (fix #390)? Not a C# `partial` part (bases may sit in
|
|
442
|
+
* another part) and not a C/C++ declaration (it may forward-declare a class
|
|
443
|
+
* defined elsewhere). Memoized per build.
|
|
444
|
+
*/
|
|
445
|
+
function fileDeclaresClass(index, name, file) {
|
|
446
|
+
const state = _state(index);
|
|
447
|
+
let files = state.declaringFiles.get(name);
|
|
448
|
+
if (!files) {
|
|
449
|
+
files = new Set();
|
|
450
|
+
for (const d of index.symbols.get(name) || []) {
|
|
451
|
+
if (!d || d.name !== name || !d.file || !INHERITANCE_DECLARATION_KINDS.has(d.type)) continue;
|
|
452
|
+
if (Array.isArray(d.modifiers) && d.modifiers.includes('partial')) continue;
|
|
453
|
+
if (langTraits(index.files.get(d.file)?.language)?.textualIncludes) continue;
|
|
454
|
+
files.add(d.file);
|
|
455
|
+
}
|
|
456
|
+
state.declaringFiles.set(name, files);
|
|
457
|
+
}
|
|
458
|
+
return files.has(file);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const INHERITANCE_DECLARATION_KINDS = new Set(['class', 'interface', 'struct', 'trait', 'record', 'enum']);
|
|
462
|
+
|
|
463
|
+
/** How many class definitions named `name` one file declares (memoized). */
|
|
464
|
+
function _sameFileNamesakeCount(index, name, file) {
|
|
465
|
+
const state = _state(index);
|
|
466
|
+
let byFile = state.namesakes.get(name);
|
|
467
|
+
if (!byFile) {
|
|
468
|
+
byFile = new Map();
|
|
469
|
+
for (const d of index.symbols.get(name) || []) {
|
|
470
|
+
if (!d || d.name !== name || !OWNER_CLASS_KINDS.has(d.type) || !d.file) continue;
|
|
471
|
+
byFile.set(d.file, (byFile.get(d.file) || 0) + 1);
|
|
472
|
+
}
|
|
473
|
+
state.namesakes.set(name, byFile);
|
|
474
|
+
}
|
|
475
|
+
return byFile.get(file) || 0;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Do two members belong to different class definitions of one file, by
|
|
480
|
+
* their certain owners (fix #389)? With `bIsOwner`, `b` is the class
|
|
481
|
+
* definition itself. false when either owner is unknown or the owners are
|
|
482
|
+
* in different files.
|
|
483
|
+
*/
|
|
484
|
+
function distinctOwnerDefinitions(index, a, b, bIsOwner = false) {
|
|
485
|
+
// Only a file declaring two same-name types can hold two owners.
|
|
486
|
+
const name = ownerNameOf(a);
|
|
487
|
+
if (!name || !a?.file || _sameFileNamesakeCount(index, name, a.file) < 2) return false;
|
|
488
|
+
const ownerA = ownerDefinitionOf(index, a);
|
|
489
|
+
const ownerB = bIsOwner ? b : ownerDefinitionOf(index, b);
|
|
490
|
+
// Declarations in two files may be one class in two configurations
|
|
491
|
+
// (#385) or two parts of one type; only scopes of one file are decided
|
|
492
|
+
// here.
|
|
493
|
+
if (!ownerA || !ownerB || ownerA.file !== ownerB.file) return false;
|
|
494
|
+
return classKeyOf(index, ownerA) !== classKeyOf(index, ownerB);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Inheritance parents of a class reference, each resolved to a definition
|
|
499
|
+
* from the child's own file (key null when unresolvable).
|
|
500
|
+
*/
|
|
501
|
+
function parentRefsOf(index, ref) {
|
|
502
|
+
if (!ref) return [];
|
|
503
|
+
const state = _state(index);
|
|
504
|
+
const cacheKey = ref.key ? `k\0${ref.key}` : null;
|
|
505
|
+
if (cacheKey && state.parents.has(cacheKey)) return state.parents.get(cacheKey);
|
|
506
|
+
// Every declaration carrying this identity contributes its own base
|
|
507
|
+
// clause: a C# partial type may declare its bases in one part only, and
|
|
508
|
+
// TS class + interface merging unions both heritage lists.
|
|
509
|
+
let decls = [];
|
|
510
|
+
if (ref.key) {
|
|
511
|
+
decls = classDefsNamed(index, ref.name).entries
|
|
512
|
+
.filter(e => e.key === ref.key).map(e => e.def);
|
|
513
|
+
}
|
|
514
|
+
if (decls.length === 0 && ref.def) decls = [ref.def];
|
|
515
|
+
const out = [];
|
|
516
|
+
const seen = new Set();
|
|
517
|
+
const push = parent => {
|
|
518
|
+
const k = parent.key || `?${parent.name}${parent.external ? '!' : ''}`;
|
|
519
|
+
if (seen.has(k)) return;
|
|
520
|
+
seen.add(k);
|
|
521
|
+
out.push(parent);
|
|
522
|
+
};
|
|
523
|
+
if (decls.length === 0) {
|
|
524
|
+
// Unknown or def-less owner (e.g. a JS constructor function):
|
|
525
|
+
// name-level parents are the only information available.
|
|
526
|
+
for (const parentName of index._getInheritanceParents(ref.name, null) || []) {
|
|
527
|
+
if (parentName) push(resolveClassRef(index, parentName, null, {}));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const anchored = (index.extendsGraph.get(ref.name) || [])
|
|
531
|
+
.some(e => typeof e === 'object' && e.startLine != null);
|
|
532
|
+
for (const decl of decls) {
|
|
533
|
+
const contextFile = decl.file;
|
|
534
|
+
let names = index._getInheritanceParentsAt(ref.name, decl.file, decl.startLine);
|
|
535
|
+
if (names == null && !anchored) {
|
|
536
|
+
// Legacy graph shape without per-def anchors.
|
|
537
|
+
names = index._getInheritanceParents(ref.name, decl.file);
|
|
538
|
+
}
|
|
539
|
+
// The graph strips a qualifier it could resolve (`com.x.b1.Base` ->
|
|
540
|
+
// `Base`); the declaration still spells it, and the qualifier is the
|
|
541
|
+
// language's own disambiguation. Re-read it from the declared clause.
|
|
542
|
+
const rawParents = decl.extends ? splitParentList(decl.extends) : [];
|
|
543
|
+
const declRef = { name: ref.name, key: ref.key, def: decl };
|
|
544
|
+
for (const parentName of names || []) {
|
|
545
|
+
if (!parentName) continue;
|
|
546
|
+
const qualified = parentName.includes('::') || parentName.includes('.');
|
|
547
|
+
if (qualified && classDefsNamed(index, parentName).entries.length === 0) {
|
|
548
|
+
push(_resolveQualifiedParent(index, parentName, contextFile, declRef));
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (!qualified) {
|
|
552
|
+
const spelled = rawParents.filter(raw =>
|
|
553
|
+
(raw.includes('::') || raw.includes('.')) &&
|
|
554
|
+
raw.split(raw.includes('::') ? '::' : '.').pop() === parentName);
|
|
555
|
+
if (spelled.length === 1) {
|
|
556
|
+
const q = _resolveQualifiedParent(index, spelled[0], contextFile, declRef);
|
|
557
|
+
if (q.key || q.external) { push(q); continue; }
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// The file may bind the name to an out-of-project type that
|
|
561
|
+
// shares it with a project type (fix #390: `import
|
|
562
|
+
// org.hamcrest.TypeSafeMatcher; ... extends TypeSafeMatcher<T>`
|
|
563
|
+
// beside a project TypeSafeMatcher): the language's own name
|
|
564
|
+
// lookup decides, as for every written type (#371/#372).
|
|
565
|
+
const denoted = externalParentDenotation(index, contextFile, parentName, decl.startLine);
|
|
566
|
+
if (denoted) {
|
|
567
|
+
push(denoted);
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
push(resolveClassRef(index, parentName, contextFile, {
|
|
571
|
+
excludeKey: parentName === ref.name ? ref.key : null,
|
|
572
|
+
namespace: decl.namespace || null,
|
|
573
|
+
...(langTraits(index.files.get(contextFile)?.language)?.genericArityIsIdentity && {
|
|
574
|
+
arity: _writtenParentArity(decl, parentName),
|
|
575
|
+
}),
|
|
576
|
+
}));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (cacheKey) state.parents.set(cacheKey, out);
|
|
580
|
+
return out;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* An unqualified base name the declaring file binds to an out-of-project
|
|
585
|
+
* type (fix #390): an external reference (or an unresolved one when the
|
|
586
|
+
* denotation is ambiguous), null when the name may denote a project type.
|
|
587
|
+
* Only names a project class also carries need the check.
|
|
588
|
+
*/
|
|
589
|
+
function externalParentDenotation(index, contextFile, parentName, line) {
|
|
590
|
+
if (!contextFile || classDefsNamed(index, parentName).entries.length === 0) return null;
|
|
591
|
+
const { externalTypeDenotation } = require('./type-denotation');
|
|
592
|
+
const denotation = externalTypeDenotation(index, contextFile, parentName, null, line);
|
|
593
|
+
if (!denotation) return null;
|
|
594
|
+
return denotation.certain === false
|
|
595
|
+
? { name: parentName, key: null, def: null }
|
|
596
|
+
: { name: parentName, key: null, def: null, external: true };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Generic arity of the base `parentName` as the declaration spells it
|
|
600
|
+
* (fix #380); undefined when the clause does not spell it exactly once. */
|
|
601
|
+
function _splitTopLevel(text) {
|
|
602
|
+
const out = [];
|
|
603
|
+
let depth = 0;
|
|
604
|
+
let start = 0;
|
|
605
|
+
const value = String(text || '');
|
|
606
|
+
for (let i = 0; i < value.length; i++) {
|
|
607
|
+
const ch = value[i];
|
|
608
|
+
if (ch === '<' || ch === '(' || ch === '[') depth++;
|
|
609
|
+
else if (ch === '>' || ch === ')' || ch === ']') depth--;
|
|
610
|
+
else if (ch === ',' && depth === 0) {
|
|
611
|
+
out.push(value.slice(start, i).trim());
|
|
612
|
+
start = i + 1;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
out.push(value.slice(start).trim());
|
|
616
|
+
return out.filter(Boolean);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function _writtenParentArity(decl, parentName) {
|
|
620
|
+
const raws = [
|
|
621
|
+
..._splitTopLevel(decl.extends),
|
|
622
|
+
...(Array.isArray(decl.implements) ? decl.implements : _splitTopLevel(decl.implements)),
|
|
623
|
+
].filter(raw => String(raw).replace(/<.*$/s, '').trim().split(/::|\./).pop() === parentName);
|
|
624
|
+
if (raws.length !== 1) return undefined;
|
|
625
|
+
const arity = genericArityOf(raws[0]);
|
|
626
|
+
return Number.isInteger(arity) ? arity : undefined;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* A parent spelled with a qualifier the inheritance graph kept verbatim
|
|
631
|
+
* (`base.Chunks`, `pkg::Base`, `Ns.Base`). Structural languages resolve the
|
|
632
|
+
* qualifier through the file's own import bindings (module or from-imported
|
|
633
|
+
* submodule); nominal ones through the declared namespace of the candidates.
|
|
634
|
+
* Returns a reference named by the TERMINAL type name (members record the
|
|
635
|
+
* bare class name), key null when the qualifier does not pin one definition.
|
|
636
|
+
*/
|
|
637
|
+
function _resolveQualifiedParent(index, parentName, contextFile, childRef) {
|
|
638
|
+
const separator = parentName.includes('::') ? '::' : '.';
|
|
639
|
+
const pieces = parentName.split(separator).filter(Boolean);
|
|
640
|
+
const terminal = pieces.pop();
|
|
641
|
+
const qualifier = pieces.join(separator);
|
|
642
|
+
const info = classDefsNamed(index, terminal);
|
|
643
|
+
let entries = info.entries;
|
|
644
|
+
if (terminal === childRef.name && childRef.key) entries = entries.filter(e => e.key !== childRef.key);
|
|
645
|
+
if (entries.length === 0) return { name: terminal, key: null, def: null, external: true };
|
|
646
|
+
const unknown = { name: terminal, key: null, def: null };
|
|
647
|
+
const fileEntry = contextFile ? index.files.get(contextFile) : null;
|
|
648
|
+
if (!fileEntry) return unknown;
|
|
649
|
+
const traits = langTraits(fileEntry.language);
|
|
650
|
+
let hit = null;
|
|
651
|
+
if (traits?.typeSystem === 'structural') {
|
|
652
|
+
if (fileEntry.language === 'python') {
|
|
653
|
+
const { pythonModulePath, pythonModuleScope, pythonModuleTypes } = require('./python-modules');
|
|
654
|
+
const memo = _state(index).modules;
|
|
655
|
+
if (!pythonModuleScope(index, contextFile, qualifier, childRef.def, memo)) return unknown;
|
|
656
|
+
const modules = pythonModulePath(index, contextFile, qualifier, memo);
|
|
657
|
+
if (!modules || modules.unknown) return unknown;
|
|
658
|
+
if (modules.files.length === 0) return { ...unknown, external: true };
|
|
659
|
+
const visible = entries.filter(e => !_functionLocal(e.def));
|
|
660
|
+
const matches = modules.files.map(file => {
|
|
661
|
+
const definitions = new Set(pythonModuleTypes(index, file, terminal, visible.map(e => e.def)));
|
|
662
|
+
return _single(visible.filter(e => definitions.has(e.def)));
|
|
663
|
+
});
|
|
664
|
+
hit = matches[0];
|
|
665
|
+
if (!hit || matches.some(match => match?.key !== hit.key)) return unknown;
|
|
666
|
+
return { name: terminal, key: hit.key, def: hit.def };
|
|
667
|
+
}
|
|
668
|
+
const binding = (fileEntry.importBindings || []).find(b => (b.alias || b.name) === pieces[0]);
|
|
669
|
+
const specs = [];
|
|
670
|
+
if (binding) {
|
|
671
|
+
specs.push(binding.module);
|
|
672
|
+
if (binding.kind === 'from') specs.push(`${binding.module}.${binding.name}`);
|
|
673
|
+
}
|
|
674
|
+
specs.push(qualifier);
|
|
675
|
+
const moduleFiles = [];
|
|
676
|
+
for (const spec of specs) {
|
|
677
|
+
const rel = spec && fileEntry.moduleResolved?.[spec];
|
|
678
|
+
if (rel) moduleFiles.push(path.join(index.root, rel));
|
|
679
|
+
}
|
|
680
|
+
for (const moduleFile of moduleFiles) {
|
|
681
|
+
const direct = entries.filter(e => e.def.file === moduleFile);
|
|
682
|
+
hit = direct.length > 0 ? _single(direct) : _transitiveImportMatch(index, moduleFile, entries);
|
|
683
|
+
if (hit) break;
|
|
684
|
+
}
|
|
685
|
+
// An import binding of an external module (fix #381): `class
|
|
686
|
+
// Channel(amqp.Channel)` under `import amqp` names amqp's class,
|
|
687
|
+
// never a same-name project class.
|
|
688
|
+
if (!hit && binding && moduleFiles.length === 0) {
|
|
689
|
+
const { _unresolvedModuleIsGap } = require('./callers');
|
|
690
|
+
if (!_unresolvedModuleIsGap(index, binding.module, binding)) {
|
|
691
|
+
return { name: terminal, key: null, def: null, external: true };
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
} else {
|
|
695
|
+
// The qualifier names the declaring namespace/package, or the
|
|
696
|
+
// enclosing TYPE of a nested type (`RecyclerPool.ThreadLocalPoolBase`,
|
|
697
|
+
// `Outer::Inner`), optionally itself namespace-qualified (fix #376).
|
|
698
|
+
const qualifierMatches = owner => owner && (owner === qualifier ||
|
|
699
|
+
owner.endsWith(`${separator}${qualifier}`) ||
|
|
700
|
+
qualifier.endsWith(`${separator}${owner}`));
|
|
701
|
+
const matches = entries.filter(e => {
|
|
702
|
+
const namespace = String(e.def.namespace || '');
|
|
703
|
+
const enclosing = String(e.def.enclosingType || '');
|
|
704
|
+
if (qualifierMatches(namespace)) return true;
|
|
705
|
+
if (!enclosing) return false;
|
|
706
|
+
return qualifierMatches(namespace ? `${namespace}${separator}${enclosing}` : enclosing) ||
|
|
707
|
+
qualifier === enclosing;
|
|
708
|
+
});
|
|
709
|
+
hit = _single(matches);
|
|
710
|
+
}
|
|
711
|
+
return hit ? { name: terminal, key: hit.key, def: hit.def } : unknown;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Every declared supertype of a class reference: the inheritance parents
|
|
716
|
+
* (parentRefsOf) plus, with `implements`, the interfaces its declarations
|
|
717
|
+
* list in a separate implements clause (Java/TypeScript), each resolved from
|
|
718
|
+
* the declaring file with the same qualifier discipline. Used by rename
|
|
719
|
+
* closure (fix #376), which must follow interface slots the dispatch graph
|
|
720
|
+
* leaves out.
|
|
721
|
+
*/
|
|
722
|
+
function supertypeRefsOf(index, ref, opts = {}) {
|
|
723
|
+
const parents = parentRefsOf(index, ref);
|
|
724
|
+
if (!opts.implements || !ref) return parents;
|
|
725
|
+
let decls = [];
|
|
726
|
+
if (ref.key) {
|
|
727
|
+
decls = classDefsNamed(index, ref.name).entries
|
|
728
|
+
.filter(e => e.key === ref.key).map(e => e.def);
|
|
729
|
+
}
|
|
730
|
+
if (decls.length === 0 && ref.def) decls = [ref.def];
|
|
731
|
+
const out = [...parents];
|
|
732
|
+
const seen = new Set(parents.map(p => p.key || `?${p.name}${p.external ? '!' : ''}`));
|
|
733
|
+
for (const decl of decls) {
|
|
734
|
+
const implemented = Array.isArray(decl.implements) ? decl.implements
|
|
735
|
+
: decl.implements ? splitParentList(decl.implements) : [];
|
|
736
|
+
for (const raw of implemented) {
|
|
737
|
+
const spelled = String(raw).replace(/<.*$/s, '').replace(/\s+/g, '').trim();
|
|
738
|
+
if (!spelled) continue;
|
|
739
|
+
const qualified = spelled.includes('::') || spelled.includes('.');
|
|
740
|
+
const bare = spelled.split(spelled.includes('::') ? '::' : '.').pop();
|
|
741
|
+
let parent;
|
|
742
|
+
if (classDefsNamed(index, bare).entries.length === 0) {
|
|
743
|
+
parent = { name: bare, key: null, def: null, external: true };
|
|
744
|
+
} else if (qualified) {
|
|
745
|
+
parent = _resolveQualifiedParent(index, spelled, decl.file,
|
|
746
|
+
{ name: ref.name, key: ref.key, def: decl });
|
|
747
|
+
} else {
|
|
748
|
+
// A written generic arity names only the type of that arity
|
|
749
|
+
// (fix #380): `: IResolver<A, B, C>` never resolves to a
|
|
750
|
+
// non-generic `IResolver` (fix #400, implements clauses).
|
|
751
|
+
const arity = langTraits(index.files.get(decl.file)?.language)?.genericArityIsIdentity
|
|
752
|
+
? genericArityOf(raw) : undefined;
|
|
753
|
+
parent = externalParentDenotation(index, decl.file, bare, decl.startLine) ||
|
|
754
|
+
resolveClassRef(index, bare, decl.file, {
|
|
755
|
+
excludeKey: bare === ref.name ? ref.key : null,
|
|
756
|
+
namespace: decl.namespace || null,
|
|
757
|
+
...(Number.isInteger(arity) && { arity }),
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
const k = parent.key || `?${parent.name}${parent.external ? '!' : ''}`;
|
|
761
|
+
if (seen.has(k)) continue;
|
|
762
|
+
seen.add(k);
|
|
763
|
+
out.push({ ...parent, viaImplements: true });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return out;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function _refVisitKey(ref) {
|
|
770
|
+
return ref.key || `?${ref.name}`;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Is member `m` owned by class reference `ref`?
|
|
775
|
+
* @returns {'yes'|'no'|'maybe'}
|
|
776
|
+
*/
|
|
777
|
+
function ownedBy(index, member, ref) {
|
|
778
|
+
const name = ownerNameOf(member);
|
|
779
|
+
if (!name || !ref || name !== ref.name) return 'no';
|
|
780
|
+
const own = ownerRefOf(index, member);
|
|
781
|
+
if (!own?.key || !ref.key) return 'maybe';
|
|
782
|
+
return own.key === ref.key ? 'yes' : 'no';
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Find the class (the caller's own, or the nearest ancestor through resolved
|
|
787
|
+
* parents) that declares a member among `definitions`.
|
|
788
|
+
* @param {object} startRef - caller's owning class reference
|
|
789
|
+
* @param {object[]} definitions - candidate member defs
|
|
790
|
+
* @param {object} [opts] - { parentsOnly }: super/base calls skip startRef
|
|
791
|
+
* @returns {{ref, uncertain, members}|null}
|
|
792
|
+
*/
|
|
793
|
+
function findDeclaringClass(index, startRef, definitions, opts = {}) {
|
|
794
|
+
if (!startRef) return null;
|
|
795
|
+
// A resolved start class fixes the whole walk: memoize per definitions
|
|
796
|
+
// list (the symbol table's array for the name) and class key.
|
|
797
|
+
if (!startRef.key || !Array.isArray(definitions)) {
|
|
798
|
+
return _findDeclaringClass(index, startRef, definitions, opts);
|
|
799
|
+
}
|
|
800
|
+
const state = _state(index);
|
|
801
|
+
let byRef = state.declaring.get(definitions);
|
|
802
|
+
if (!byRef) { byRef = new Map(); state.declaring.set(definitions, byRef); }
|
|
803
|
+
const memoKey = `${opts.parentsOnly ? 'p' : 'o'}\0${startRef.key}`;
|
|
804
|
+
if (byRef.has(memoKey)) return byRef.get(memoKey);
|
|
805
|
+
const result = _findDeclaringClass(index, startRef, definitions, opts);
|
|
806
|
+
byRef.set(memoKey, result);
|
|
807
|
+
return result;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function _findDeclaringClass(index, startRef, definitions, opts) {
|
|
811
|
+
const check = ref => {
|
|
812
|
+
let uncertain = false;
|
|
813
|
+
const members = [];
|
|
814
|
+
for (const d of definitions) {
|
|
815
|
+
const v = ownedBy(index, d, ref);
|
|
816
|
+
if (v === 'no') continue;
|
|
817
|
+
if (v === 'maybe') uncertain = true;
|
|
818
|
+
members.push(d);
|
|
819
|
+
}
|
|
820
|
+
return members.length > 0 ? { ref, uncertain, members } : null;
|
|
821
|
+
};
|
|
822
|
+
const visited = new Set([_refVisitKey(startRef)]);
|
|
823
|
+
if (!opts.parentsOnly) {
|
|
824
|
+
const own = check(startRef);
|
|
825
|
+
if (own) return own;
|
|
826
|
+
}
|
|
827
|
+
const queue = [...parentRefsOf(index, startRef)];
|
|
828
|
+
while (queue.length > 0) {
|
|
829
|
+
const ref = queue.shift();
|
|
830
|
+
if (ref.external) continue;
|
|
831
|
+
const vk = _refVisitKey(ref);
|
|
832
|
+
if (visited.has(vk)) continue;
|
|
833
|
+
visited.add(vk);
|
|
834
|
+
const hit = check(ref);
|
|
835
|
+
if (hit) {
|
|
836
|
+
// An unresolved parent can only yield an uncertain match.
|
|
837
|
+
if (!ref.key) hit.uncertain = true;
|
|
838
|
+
return hit;
|
|
839
|
+
}
|
|
840
|
+
if (ref.key || ref.def) {
|
|
841
|
+
for (const p of parentRefsOf(index, ref)) {
|
|
842
|
+
if (!visited.has(_refVisitKey(p))) queue.push(p);
|
|
843
|
+
}
|
|
844
|
+
} else {
|
|
845
|
+
// Unresolved parent name: follow name-level parents so deeper
|
|
846
|
+
// ancestors still surface (their matches stay uncertain).
|
|
847
|
+
for (const p of index._getInheritanceParents(ref.name, null) || []) {
|
|
848
|
+
const pr = { name: p, key: null, def: null };
|
|
849
|
+
if (!visited.has(_refVisitKey(pr))) queue.push(pr);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Relationship of class reference `ref` to the owners of `targetDefs`:
|
|
858
|
+
* 'same' (identical definition), 'ancestor' (ref is a transitive ancestor of
|
|
859
|
+
* a target owner through resolved parents), 'maybe' (a name-equal owner or
|
|
860
|
+
* ancestor could not be resolved to a definition), or 'unrelated'.
|
|
861
|
+
*/
|
|
862
|
+
function relationToTargets(index, ref, targetDefs) {
|
|
863
|
+
let maybe = false;
|
|
864
|
+
let ancestor = false;
|
|
865
|
+
for (const td of targetDefs) {
|
|
866
|
+
const tRef = ownerRefOf(index, td);
|
|
867
|
+
if (!tRef) continue;
|
|
868
|
+
if (tRef.name === ref.name) {
|
|
869
|
+
if (tRef.key && ref.key && tRef.key === ref.key) return 'same';
|
|
870
|
+
if (!tRef.key || !ref.key) maybe = true;
|
|
871
|
+
}
|
|
872
|
+
const visited = new Set([_refVisitKey(tRef)]);
|
|
873
|
+
const queue = [...parentRefsOf(index, tRef)];
|
|
874
|
+
while (queue.length > 0 && !ancestor) {
|
|
875
|
+
const p = queue.shift();
|
|
876
|
+
if (p.external) continue;
|
|
877
|
+
const vk = _refVisitKey(p);
|
|
878
|
+
if (visited.has(vk)) continue;
|
|
879
|
+
visited.add(vk);
|
|
880
|
+
if (p.name === ref.name) {
|
|
881
|
+
if (p.key && ref.key && p.key === ref.key) { ancestor = true; break; }
|
|
882
|
+
if (!p.key || !ref.key) maybe = true;
|
|
883
|
+
}
|
|
884
|
+
if (p.key || p.def) {
|
|
885
|
+
queue.push(...parentRefsOf(index, p));
|
|
886
|
+
} else {
|
|
887
|
+
for (const n of index._getInheritanceParents(p.name, null) || []) {
|
|
888
|
+
queue.push({ name: n, key: null, def: null });
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (ancestor) return 'ancestor';
|
|
894
|
+
return maybe ? 'maybe' : 'unrelated';
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Is class reference `ref` a target owner or a descendant of one, by
|
|
899
|
+
* definition identity (fix #390)? Walks `ref`'s resolved ancestry upward:
|
|
900
|
+
* 'yes' when it reaches a target owner's definition, 'maybe' when a same-name
|
|
901
|
+
* owner or ancestor on the way cannot be resolved, 'no' otherwise. A class
|
|
902
|
+
* named like a subclass of the target elsewhere (`org.x.MoneyTest` beside
|
|
903
|
+
* `junit.x.MoneyTest extends TestCase`) is 'no'.
|
|
904
|
+
*/
|
|
905
|
+
function descendsFromTargets(index, ref, targetDefs) {
|
|
906
|
+
if (!ref) return 'maybe';
|
|
907
|
+
const targets = [];
|
|
908
|
+
for (const td of targetDefs || []) {
|
|
909
|
+
const tRef = ownerRefOf(index, td);
|
|
910
|
+
if (tRef) targets.push(tRef);
|
|
911
|
+
}
|
|
912
|
+
if (targets.length === 0) return 'maybe';
|
|
913
|
+
let maybe = false;
|
|
914
|
+
const matches = candidate => {
|
|
915
|
+
for (const t of targets) {
|
|
916
|
+
if (t.name !== candidate.name) continue;
|
|
917
|
+
if (t.key && candidate.key && t.key === candidate.key) return true;
|
|
918
|
+
if (!t.key || !candidate.key) maybe = true;
|
|
919
|
+
}
|
|
920
|
+
return false;
|
|
921
|
+
};
|
|
922
|
+
if (matches(ref)) return 'yes';
|
|
923
|
+
const visited = new Set([_refVisitKey(ref)]);
|
|
924
|
+
const queue = [...parentRefsOf(index, ref)];
|
|
925
|
+
while (queue.length > 0) {
|
|
926
|
+
const parent = queue.shift();
|
|
927
|
+
if (parent.external) continue;
|
|
928
|
+
const vk = _refVisitKey(parent);
|
|
929
|
+
if (visited.has(vk)) continue;
|
|
930
|
+
visited.add(vk);
|
|
931
|
+
if (matches(parent)) return 'yes';
|
|
932
|
+
if (parent.key || parent.def) {
|
|
933
|
+
queue.push(...parentRefsOf(index, parent));
|
|
934
|
+
} else {
|
|
935
|
+
for (const name of index._getInheritanceParents(parent.name, null) || []) {
|
|
936
|
+
queue.push({ name, key: null, def: null });
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return maybe ? 'maybe' : 'no';
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Keys of a class definition and its transitive project descendants
|
|
945
|
+
* (children resolved back through their own parent refs, so a same-name
|
|
946
|
+
* class elsewhere never counts). Null when the reference is unresolved.
|
|
947
|
+
*/
|
|
948
|
+
function descendantKeys(index, ref, cap = 256) {
|
|
949
|
+
if (!ref?.key) return null;
|
|
950
|
+
if (cap !== 256) return _descendantKeys(index, ref, cap);
|
|
951
|
+
const state = _state(index);
|
|
952
|
+
let keys = state.descendants.get(ref.key);
|
|
953
|
+
if (!keys) {
|
|
954
|
+
keys = _descendantKeys(index, ref, cap);
|
|
955
|
+
state.descendants.set(ref.key, keys);
|
|
956
|
+
}
|
|
957
|
+
return keys;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function _descendantKeys(index, ref, cap) {
|
|
961
|
+
const out = new Set([ref.key]);
|
|
962
|
+
const queue = [ref];
|
|
963
|
+
while (queue.length > 0 && out.size < cap) {
|
|
964
|
+
const cur = queue.shift();
|
|
965
|
+
for (const child of index.extendedByGraph?.get(cur.name) || []) {
|
|
966
|
+
const cName = typeof child === 'string' ? child : child.name;
|
|
967
|
+
if (!cName) continue;
|
|
968
|
+
const info = classDefsNamed(index, cName);
|
|
969
|
+
const candidates = info.entries.filter(e =>
|
|
970
|
+
typeof child === 'string' || !child.file || e.def.file === child.file);
|
|
971
|
+
for (const e of candidates) {
|
|
972
|
+
if (out.has(e.key)) continue;
|
|
973
|
+
const childRef = { name: cName, key: e.key, def: e.def };
|
|
974
|
+
const extendsCur = parentRefsOf(index, childRef).some(p =>
|
|
975
|
+
p.name === cur.name && !p.external &&
|
|
976
|
+
(!p.key || p.key === cur.key));
|
|
977
|
+
if (!extendsCur) continue;
|
|
978
|
+
out.add(e.key);
|
|
979
|
+
queue.push(childRef);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return out;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Python MRO trap (#202b guard) by definition identity: do the matched class
|
|
988
|
+
* and a target owner share a project descendant? A descendant of BOTH sides
|
|
989
|
+
* can route `self.m()` in the matched class to the target through its MRO.
|
|
990
|
+
* Matched BELOW the target is not a trap (the subclass override wins).
|
|
991
|
+
* Unresolved identities answer true (conservative: never licenses exclusion).
|
|
992
|
+
*/
|
|
993
|
+
function shareDescendant(index, matchedRef, targetDefs) {
|
|
994
|
+
const mine = descendantKeys(index, matchedRef);
|
|
995
|
+
if (!mine) return true;
|
|
996
|
+
for (const td of targetDefs) {
|
|
997
|
+
const tRef = ownerRefOf(index, td);
|
|
998
|
+
if (!tRef) continue;
|
|
999
|
+
const theirs = descendantKeys(index, tRef);
|
|
1000
|
+
if (!theirs) return true;
|
|
1001
|
+
if (theirs.has(matchedRef.key)) continue;
|
|
1002
|
+
for (const k of theirs) if (mine.has(k)) return true;
|
|
1003
|
+
}
|
|
1004
|
+
return false;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
module.exports = {
|
|
1008
|
+
OWNER_CLASS_KINDS,
|
|
1009
|
+
resetClassIdentity,
|
|
1010
|
+
ownerNameOf,
|
|
1011
|
+
classKeyOf,
|
|
1012
|
+
classDefsNamed,
|
|
1013
|
+
resolveClassRef,
|
|
1014
|
+
resolveQualifiedClassRef: _resolveQualifiedParent,
|
|
1015
|
+
ownerRefOf,
|
|
1016
|
+
ownerDefinitionOf,
|
|
1017
|
+
distinctOwnerDefinitions,
|
|
1018
|
+
hasFunctionLocalClass,
|
|
1019
|
+
fileDeclaresClass,
|
|
1020
|
+
parentRefsOf,
|
|
1021
|
+
supertypeRefsOf,
|
|
1022
|
+
ownedBy,
|
|
1023
|
+
findDeclaringClass,
|
|
1024
|
+
relationToTargets,
|
|
1025
|
+
descendsFromTargets,
|
|
1026
|
+
descendantKeys,
|
|
1027
|
+
shareDescendant,
|
|
1028
|
+
};
|