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.
Files changed (85) hide show
  1. package/.claude/skills/ucn/SKILL.md +58 -236
  2. package/.claude/skills/ucn/references/commands.md +144 -68
  3. package/.claude/skills/ucn/references/resolution-rules.md +171 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +27 -33
  5. package/README.md +27 -16
  6. package/cli/index.js +45 -14
  7. package/core/accessors.js +118 -3
  8. package/core/account.js +62 -11
  9. package/core/analysis.js +517 -266
  10. package/core/ast-analysis.js +76 -117
  11. package/core/async-producers.js +1729 -0
  12. package/core/bridge.js +1877 -268
  13. package/core/brief.js +6 -0
  14. package/core/build-worker.js +78 -14
  15. package/core/cache.js +529 -31
  16. package/core/callers.js +12228 -921
  17. package/core/check.js +30 -38
  18. package/core/class-identity.js +1028 -0
  19. package/core/command-contracts.js +6 -6
  20. package/core/configuration-items.js +86 -0
  21. package/core/contract-membership.js +2262 -0
  22. package/core/cpp-scope.js +532 -0
  23. package/core/deadcode.js +171 -49
  24. package/core/decorator-binding.js +260 -0
  25. package/core/discovery.js +281 -164
  26. package/core/errors.js +52 -0
  27. package/core/execute.js +21 -9
  28. package/core/external-macros.js +326 -0
  29. package/core/graph-build.js +277 -8
  30. package/core/graph.js +35 -2
  31. package/core/ignore-rules.js +472 -0
  32. package/core/imports.js +504 -42
  33. package/core/index-ir.js +94 -11
  34. package/core/inprocess-clients.js +636 -0
  35. package/core/ir.js +87 -8
  36. package/core/macro-expansion.js +1416 -0
  37. package/core/output/analysis.js +18 -2
  38. package/core/output/brief.js +3 -0
  39. package/core/output/doctor.js +4 -0
  40. package/core/output/endpoints.js +15 -6
  41. package/core/output/find.js +7 -2
  42. package/core/output/refactoring.js +121 -14
  43. package/core/output/reporting.js +31 -3
  44. package/core/output/shared.js +14 -0
  45. package/core/output-budget.js +305 -122
  46. package/core/parallel-build.js +124 -47
  47. package/core/parser.js +3 -2
  48. package/core/project.js +472 -66
  49. package/core/provenance-facts.js +156 -12
  50. package/core/provenance-overload.js +30 -3
  51. package/core/provenance.js +104 -30
  52. package/core/pytest-fixtures.js +434 -0
  53. package/core/python-fixture-flow.js +17 -3
  54. package/core/python-modules.js +173 -0
  55. package/core/reflection.js +877 -0
  56. package/core/registry.js +1 -1
  57. package/core/reporting.js +234 -52
  58. package/core/request-helpers.js +561 -0
  59. package/core/route-graph.js +1922 -0
  60. package/core/route-lists.js +2414 -0
  61. package/core/rust-macro-expansion.js +3026 -0
  62. package/core/rust-modules.js +203 -0
  63. package/core/search.js +121 -29
  64. package/core/shared.js +19 -1
  65. package/core/test-name-bindings.js +256 -0
  66. package/core/type-denotation.js +1185 -0
  67. package/core/type-references.js +3229 -0
  68. package/core/verify.js +3360 -672
  69. package/languages/adapter.js +31 -5
  70. package/languages/c-family.js +4061 -212
  71. package/languages/c-macro-invocations.js +380 -0
  72. package/languages/c-preprocessor.js +254 -0
  73. package/languages/csharp.js +1406 -49
  74. package/languages/go.js +1243 -70
  75. package/languages/index.js +728 -6
  76. package/languages/java.js +473 -61
  77. package/languages/javascript.js +1270 -197
  78. package/languages/lexical-scope.js +1280 -0
  79. package/languages/python.js +1557 -72
  80. package/languages/rust-macro-rules.js +1020 -0
  81. package/languages/rust-value-flow.js +416 -0
  82. package/languages/rust.js +2265 -112
  83. package/languages/utils.js +298 -4
  84. package/mcp/server.js +22 -8
  85. 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
+ };