ucn 5.4.1 → 5.5.0

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 (87) 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 +18 -12
  6. package/cli/index.js +42 -16
  7. package/core/accessors.js +118 -3
  8. package/core/account.js +30 -0
  9. package/core/analysis.js +508 -264
  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 +522 -31
  16. package/core/callers.js +12053 -1047
  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-ext.js +6 -6
  38. package/core/output/analysis.js +18 -2
  39. package/core/output/brief.js +3 -0
  40. package/core/output/doctor.js +4 -0
  41. package/core/output/endpoints.js +15 -6
  42. package/core/output/find.js +11 -2
  43. package/core/output/lines.js +8 -2
  44. package/core/output/refactoring.js +121 -14
  45. package/core/output/reporting.js +31 -3
  46. package/core/output/shared.js +14 -0
  47. package/core/output-budget.js +316 -122
  48. package/core/parallel-build.js +124 -47
  49. package/core/parser.js +3 -2
  50. package/core/project.js +457 -60
  51. package/core/provenance-facts.js +156 -12
  52. package/core/provenance-overload.js +30 -3
  53. package/core/provenance.js +104 -30
  54. package/core/pytest-fixtures.js +434 -0
  55. package/core/python-fixture-flow.js +17 -3
  56. package/core/python-modules.js +173 -0
  57. package/core/reflection.js +877 -0
  58. package/core/registry.js +1 -1
  59. package/core/reporting.js +234 -52
  60. package/core/request-helpers.js +561 -0
  61. package/core/route-graph.js +1922 -0
  62. package/core/route-lists.js +2414 -0
  63. package/core/rust-macro-expansion.js +2113 -0
  64. package/core/rust-modules.js +185 -0
  65. package/core/search.js +121 -29
  66. package/core/shared.js +19 -1
  67. package/core/test-name-bindings.js +256 -0
  68. package/core/type-denotation.js +1161 -0
  69. package/core/type-references.js +3229 -0
  70. package/core/verify.js +3360 -672
  71. package/languages/adapter.js +31 -5
  72. package/languages/c-family.js +4061 -212
  73. package/languages/c-macro-invocations.js +380 -0
  74. package/languages/c-preprocessor.js +254 -0
  75. package/languages/csharp.js +1406 -49
  76. package/languages/go.js +1243 -70
  77. package/languages/index.js +728 -6
  78. package/languages/java.js +473 -61
  79. package/languages/javascript.js +1270 -197
  80. package/languages/lexical-scope.js +1280 -0
  81. package/languages/python.js +1557 -72
  82. package/languages/rust-macro-rules.js +1020 -0
  83. package/languages/rust-value-flow.js +416 -0
  84. package/languages/rust.js +2237 -115
  85. package/languages/utils.js +298 -4
  86. package/mcp/server.js +25 -11
  87. package/package.json +1 -1
@@ -0,0 +1,2262 @@
1
+ /**
2
+ * core/contract-membership.js - Contract membership of a member definition
3
+ * (fix #360).
4
+ *
5
+ * A method is rarely just a symbol: it can fill a SLOT of a contract - an
6
+ * interface/trait/base-class member it implements or overrides, or a method
7
+ * set a type must keep to satisfy an interface. Renaming it is then a change
8
+ * to that contract, and the rename is only complete when every member of the
9
+ * contract that the project owns changes together. When part of the
10
+ * contract lives outside the project (std traits, JDK/BCL interfaces,
11
+ * framework base classes, Go interfaces from other modules), the rename
12
+ * cannot be completed by editing project code at all.
13
+ *
14
+ * This module answers, per definition:
15
+ * - hierarchySlotClosure(): the project classes a member's dispatch slot
16
+ * spans (transitive ancestors that declare it, through intermediate
17
+ * classes and declared `implements` clauses, and every descendant
18
+ * override), by class definition identity;
19
+ * - rustTraitResolution(): what a Rust trait spelling binds in its scope,
20
+ * including configuration alternatives with an external member;
21
+ * - externalContractOf(): whether the member provably or possibly fills a
22
+ * slot of an OUT-OF-PROJECT contract, with the contract names and the
23
+ * source sites a compiler would report;
24
+ * - goContractClosure(): Go's structural (implicit) satisfaction, computed
25
+ * from complete method sets and interface requirement sets, closed over
26
+ * interface embedding and promoted methods, plus the compiler evidence
27
+ * (interface conversions) that a type is used as an interface whose
28
+ * method set UCN cannot see.
29
+ *
30
+ * Everything is computed from indexed AST facts, memoized on the index and
31
+ * reset at every build/load (index._contractIndex). Unknown never counts as
32
+ * proof in either direction: an open method set or unresolved contract is
33
+ * reported for review, never silently joined or silently dropped.
34
+ */
35
+
36
+ const path = require('path');
37
+ const { langTraits } = require('../languages');
38
+ const { NON_CALLABLE_TYPES, isOverrideMarked } = require('./shared');
39
+ const { splitParentList } = require('./graph-build');
40
+
41
+ const CLASS_KINDS = new Set(['class', 'struct', 'interface', 'trait', 'record', 'enum']);
42
+
43
+ function _state(index) {
44
+ if (!index._contractIndex) {
45
+ index._contractIndex = {
46
+ go: null,
47
+ implementedBy: null,
48
+ goQualifier: new Map(),
49
+ };
50
+ }
51
+ return index._contractIndex;
52
+ }
53
+
54
+ function resetContractIndex(index) {
55
+ index._contractIndex = null;
56
+ index._languageFeatureMemo = null;
57
+ }
58
+
59
+ function _rel(index, file) {
60
+ return index.files.get(file)?.relativePath || file;
61
+ }
62
+
63
+ function _bareTypeName(raw) {
64
+ return String(raw || '').trim()
65
+ .replace(/[<[(].*$/s, '')
66
+ .split('::').pop()
67
+ .split('.').pop()
68
+ .trim();
69
+ }
70
+
71
+ function _projectTypeDefs(index, bare, fromDef = null) {
72
+ return (index.symbols.get(bare) || []).filter(d => CLASS_KINDS.has(d.type) &&
73
+ // A class never resolves its own qualified external base to itself
74
+ // (`class EnvironBuilder(werkzeug.test.EnvironBuilder)`).
75
+ !(fromDef && d.file === fromDef.file && d.startLine === fromDef.startLine &&
76
+ d.type === fromDef.type));
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Declared heritage: extends + implements, per class definition.
81
+ // ---------------------------------------------------------------------------
82
+
83
+ /** Class definitions owning a member (same file, containing range first). */
84
+ function ownerClassDefs(index, def) {
85
+ if (!def?.className) return [];
86
+ const defs = (index.symbols.get(def.className) || []).filter(d =>
87
+ CLASS_KINDS.has(d.type) && d.file === def.file);
88
+ const containing = defs.filter(d => d.startLine <= def.startLine &&
89
+ (d.endLine || d.startLine) >= (def.endLine || def.startLine));
90
+ return containing.length > 0 ? containing : defs;
91
+ }
92
+
93
+ /** Is the member declared lexically inside its owning class body? */
94
+ function memberInClassBody(index, def) {
95
+ if (!def?.className) return false;
96
+ return ownerClassDefs(index, def).some(cd => cd.startLine <= def.startLine &&
97
+ (cd.endLine || cd.startLine) >= def.startLine &&
98
+ !(cd.startLine === def.startLine && cd.name === def.name));
99
+ }
100
+
101
+ /** Raw supertype spellings of one class definition (extends + implements). */
102
+ function declaredSupertypes(def) {
103
+ const out = [];
104
+ if (def.extends) {
105
+ out.push(...(Array.isArray(def.extends) ? def.extends : splitParentList(def.extends)));
106
+ }
107
+ if (Array.isArray(def.implements)) out.push(...def.implements);
108
+ else if (def.implements) out.push(...splitParentList(def.implements));
109
+ return out.map(item => String(item).trim()).filter(Boolean);
110
+ }
111
+
112
+ /** Reverse `implements` index: interface name -> implementing class identities. */
113
+ function implementedBy(index, interfaceName) {
114
+ const state = _state(index);
115
+ if (!state.implementedBy) {
116
+ const map = new Map();
117
+ for (const [, fileEntry] of index.files) {
118
+ for (const def of fileEntry.symbols || []) {
119
+ if (!CLASS_KINDS.has(def.type) || !def.implements) continue;
120
+ const implemented = Array.isArray(def.implements) ? def.implements
121
+ : splitParentList(def.implements);
122
+ for (const raw of implemented) {
123
+ const bare = _bareTypeName(raw);
124
+ if (!bare) continue;
125
+ if (!map.has(bare)) map.set(bare, []);
126
+ map.get(bare).push({ name: def.name, file: def.file });
127
+ }
128
+ }
129
+ }
130
+ state.implementedBy = map;
131
+ }
132
+ return state.implementedBy.get(interfaceName) || [];
133
+ }
134
+
135
+ /**
136
+ * Walk a class's declared heritage (extends through project types, plus
137
+ * implements) and collect the supertypes that are NOT project types, with
138
+ * whether any project ancestor declares `memberName`.
139
+ */
140
+ function heritageOf(index, classDefs, memberName) {
141
+ const external = [];
142
+ let projectDefiner = null;
143
+ const seen = new Set();
144
+ let frontier = classDefs.map(d => ({ def: d }));
145
+ for (let depth = 0; depth < 8 && frontier.length > 0; depth++) {
146
+ const next = [];
147
+ for (const { def } of frontier) {
148
+ for (const raw of declaredSupertypes(def)) {
149
+ const bare = _bareTypeName(raw);
150
+ if (!bare) continue;
151
+ const key = `${bare}\0${def.file}`;
152
+ if (seen.has(key)) continue;
153
+ seen.add(key);
154
+ const parents = _projectTypeDefs(index, bare, def);
155
+ if (parents.length === 0) {
156
+ const spelled = String(raw).replace(/\s+/g, ' ').trim();
157
+ if (!external.includes(spelled)) external.push(spelled);
158
+ continue;
159
+ }
160
+ const parentFile = index._resolveClassFile(bare, def.file);
161
+ const resolved = parents.filter(p => p.file === parentFile);
162
+ for (const parent of (resolved.length > 0 ? resolved : parents)) {
163
+ if (!projectDefiner && memberName) {
164
+ const member = (index.symbols.get(memberName) || []).find(m =>
165
+ m.className === parent.name && m.file === parent.file &&
166
+ !NON_CALLABLE_TYPES.has(m.type));
167
+ if (member) projectDefiner = member;
168
+ }
169
+ next.push({ def: parent });
170
+ }
171
+ }
172
+ }
173
+ frontier = next;
174
+ }
175
+ return { external, projectDefiner };
176
+ }
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // Rust traits
180
+ // ---------------------------------------------------------------------------
181
+
182
+ const RUST_EXTERNAL_ROOTS = new Set(['std', 'core', 'alloc']);
183
+
184
+ /**
185
+ * What a Rust trait spelling denotes from `fromFile`, by the file's own
186
+ * scope (fix #376): project trait definitions it can bind, and whether some
187
+ * binding of it is an out-of-project trait. `use` declarations of the name
188
+ * are followed into the project module they resolve to (`crate::StdError`
189
+ * -> the crate root's own `use std::error::Error as StdError` and its
190
+ * `trait StdError` under another cfg), so configuration alternatives with
191
+ * one external member are visible. `null` when the file neither declares
192
+ * nor imports the name by a resolvable `use` (glob imports, prelude):
193
+ * callers fall back to name-level resolution.
194
+ */
195
+ function _rustModuleScope(index, file, line) {
196
+ if (line == null) return 0;
197
+ let best = null;
198
+ for (const d of index.files.get(file)?.symbols || []) {
199
+ if (d.type !== 'module' || d.startLine >= line || (d.endLine || d.startLine) < line) continue;
200
+ if (!best || d.startLine > best.startLine) best = d;
201
+ }
202
+ return best ? best.startLine : 0;
203
+ }
204
+
205
+ function rustTraitResolution(index, traitName, fromFile, depth = 0, fromLine = null) {
206
+ const spelled = String(traitName || '').replace(/<.*$/s, '').replace(/\s+/g, '').trim();
207
+ const segments = spelled.split('::').filter(Boolean);
208
+ const bare = segments[segments.length - 1];
209
+ if (!bare) return null;
210
+ if (segments.length > 1 && RUST_EXTERNAL_ROOTS.has(segments[0])) {
211
+ return { project: [], external: true };
212
+ }
213
+ const fileEntry = index.files.get(fromFile);
214
+ if (!fileEntry || depth > 6) return null;
215
+ const resolveModuleFile = spec => {
216
+ const rel = fileEntry.moduleResolved?.[spec];
217
+ return rel ? (path.isAbsolute(rel) ? rel : path.join(index.root, rel)) : null;
218
+ };
219
+ if (segments.length > 1) {
220
+ const target = resolveModuleFile(spelled);
221
+ if (!target) return null;
222
+ if (target === fromFile) return null;
223
+ return rustTraitResolution(index, bare, target, depth + 1);
224
+ }
225
+ // Only declarations in the SAME module scope as the naming site: an
226
+ // inline `mod ext { pub trait StdError }` is not the file's StdError.
227
+ const scope = _rustModuleScope(index, fromFile, fromLine);
228
+ const project = (index.symbols.get(bare) || []).filter(d =>
229
+ d.type === 'trait' && d.file === fromFile &&
230
+ _rustModuleScope(index, fromFile, d.startLine) === scope);
231
+ let external = false;
232
+ let bound = project.length > 0;
233
+ for (const binding of fileEntry.importBindings || []) {
234
+ if (binding.kind !== 'use' || (binding.alias || binding.name) !== bare) continue;
235
+ if (binding.line != null && _rustModuleScope(index, fromFile, binding.line) !== scope) continue;
236
+ const module = String(binding.module || '');
237
+ const root = module.split('::')[0];
238
+ bound = true;
239
+ if (RUST_EXTERNAL_ROOTS.has(root)) { external = true; continue; }
240
+ const target = resolveModuleFile(module);
241
+ if (!target) {
242
+ // A crate-relative path that resolves nowhere stays unknown; any
243
+ // other root names another crate.
244
+ if (!['crate', 'self', 'super'].includes(root)) external = true;
245
+ continue;
246
+ }
247
+ if (target === fromFile) continue;
248
+ const inner = rustTraitResolution(index, binding.name, target, depth + 1);
249
+ if (!inner) {
250
+ project.push(...(index.symbols.get(binding.name) || []).filter(d =>
251
+ d.type === 'trait' && d.file === target));
252
+ continue;
253
+ }
254
+ project.push(...inner.project);
255
+ if (inner.external) external = true;
256
+ }
257
+ if (!bound) return null;
258
+ return { project: [...new Set(project)], external };
259
+ }
260
+
261
+ /** Resolve a Rust impl's trait spelling to project trait definitions. */
262
+ function rustProjectTraits(index, traitName, fromFile, fromLine = null) {
263
+ const spelled = String(traitName || '').replace(/<.*$/s, '').trim();
264
+ const segments = spelled.split('::').map(s => s.trim()).filter(Boolean);
265
+ if (segments.length > 1 && RUST_EXTERNAL_ROOTS.has(segments[0])) return [];
266
+ const bare = segments[segments.length - 1];
267
+ if (!bare) return [];
268
+ const scoped = fromFile ? rustTraitResolution(index, spelled, fromFile, 0, fromLine) : null;
269
+ if (scoped && (scoped.project.length > 0 || scoped.external)) return scoped.project;
270
+ const traits = (index.symbols.get(bare) || []).filter(d => d.type === 'trait');
271
+ if (traits.length <= 1) return traits;
272
+ const file = index._resolveClassFile(bare, fromFile);
273
+ const pinned = traits.filter(d => d.file === file);
274
+ return pinned.length > 0 ? pinned : traits;
275
+ }
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // External contract membership (all languages except Go, which is structural
279
+ // and handled by goContractClosure)
280
+ // ---------------------------------------------------------------------------
281
+
282
+ function _arity(def) {
283
+ if (Array.isArray(def.paramsStructured)) {
284
+ return def.paramsStructured.filter(p => !['self', '&self', '&mut self',
285
+ 'mut self', 'this', 'cls'].includes(String(p.name || '').trim())).length;
286
+ }
287
+ return null;
288
+ }
289
+
290
+ /**
291
+ * Is `def` a member the language runtime or compiler invokes by name
292
+ * (trait `languageProtocolMember`, fix #360/#363)? Owner-gated rules (Python
293
+ * Enum hooks, Java serialization callbacks) require the owning class's
294
+ * heritage closure to reach one of the named out-of-project types.
295
+ */
296
+ function protocolMemberOf(index, def) {
297
+ return !!protocolMemberCertainty(index, def);
298
+ }
299
+
300
+ /**
301
+ * 'definite' when the owner's heritage reaches the protocol's named types,
302
+ * 'possible' when it leaves the project through another out-of-project
303
+ * supertype that may reach them (fix #389), else null.
304
+ */
305
+ function protocolMemberCertainty(index, def) {
306
+ if (!def || !def.name) return null;
307
+ const lang = index.files.get(def.file)?.language;
308
+ const predicate = langTraits(lang)?.languageProtocolMember;
309
+ if (typeof predicate !== 'function') return null;
310
+ const isMember = !!def.className;
311
+ const verdict = predicate(def.name, {
312
+ isMember,
313
+ isField: def.type === 'field' || def.type === 'property' || def.type === 'constant',
314
+ modifiers: def.modifiers || [],
315
+ paramTypes: Array.isArray(def.paramsStructured)
316
+ ? def.paramsStructured.map(param => _bareTypeName(_paramTypeText(param)) || null) : null,
317
+ derivesFrom: owners => ownerDerivesFromExternal(index, def, owners),
318
+ externalSupertypes: () => ownerExternalSupertypes(index, def),
319
+ ownerKinds: () => ownerClassDefs(index, def).map(owner => owner.type),
320
+ });
321
+ if (verdict === true) return 'definite';
322
+ return verdict || null;
323
+ }
324
+
325
+ /**
326
+ * The out-of-project supertypes the owner's heritage reaches: the bare name
327
+ * when written unqualified and not imported (the language's implicit
328
+ * scope), else the qualified spelling (fix #389).
329
+ */
330
+ function ownerExternalSupertypes(index, def) {
331
+ if (!def?.className) return [];
332
+ const owners = ownerClassDefs(index, def);
333
+ const { external } = heritageOf(index, owners, null);
334
+ const bindings = owners.flatMap(owner => index.files.get(owner.file)?.importBindings || []);
335
+ return external.map(spelled => {
336
+ const bare = _bareTypeName(spelled);
337
+ if (!bare) return null;
338
+ const head = String(spelled).replace(/<.*$/s, '').trim();
339
+ if (head !== bare) return head;
340
+ const imported = bindings.find(binding => (binding.alias || binding.name) === bare);
341
+ if (!imported) return bare;
342
+ const module = String(imported.module || '');
343
+ return module === bare || module.endsWith(`.${bare}`) ? module : `${module}.${bare}`;
344
+ }).filter(Boolean);
345
+ }
346
+
347
+ /**
348
+ * fix #380: is `def` a compiler-required type the project declares for a
349
+ * language feature it uses (trait languageProtocolType; C# IsExternalInit
350
+ * with `init` accessors or positional records), or a member of one? The
351
+ * compiler references it by full name, so no call or reference names it.
352
+ */
353
+ function protocolTypeOf(index, def) {
354
+ if (!def || !def.name) return false;
355
+ const lang = index.files.get(def.file)?.language;
356
+ const predicate = langTraits(lang)?.languageProtocolType;
357
+ if (typeof predicate !== 'function') return false;
358
+ let typeDef = def;
359
+ if (def.className) {
360
+ typeDef = ownerClassDefs(index, def)[0];
361
+ if (!typeDef) return false;
362
+ }
363
+ let features = index._languageFeatureMemo;
364
+ if (!features || features.language !== lang) {
365
+ const set = new Set();
366
+ for (const [, entry] of index.files) {
367
+ if (entry.language !== lang) continue;
368
+ for (const feature of entry.languageFeatures || []) set.add(feature);
369
+ }
370
+ features = { language: lang, set };
371
+ index._languageFeatureMemo = features;
372
+ }
373
+ return predicate(typeDef.name, {
374
+ namespace: typeDef.namespace || '',
375
+ hasFeature: feature => features.set.has(feature),
376
+ });
377
+ }
378
+
379
+ /**
380
+ * Does the class owning member `def` reach, through its heritage closure
381
+ * (extends + implements, transitively through project types), an
382
+ * out-of-project supertype whose bare name is one of `names`?
383
+ */
384
+ function ownerDerivesFromExternal(index, def, names) {
385
+ if (!def?.className) return false;
386
+ const wanted = new Set(names);
387
+ const { external } = heritageOf(index, ownerClassDefs(index, def), null);
388
+ return external.some(spelled => wanted.has(_bareTypeName(spelled)));
389
+ }
390
+
391
+ /** The standard-library namespace a declaration is written in (its
392
+ * language's `standardPathRoots`: C# `System`, C++ `std`), or null. */
393
+ function standardNamespaceOf(index, def) {
394
+ const namespace = String(def.namespace || '');
395
+ if (!namespace) return null;
396
+ const roots = langTraits(index.files.get(def.file)?.language)?.standardPathRoots || [];
397
+ for (const root of roots) {
398
+ if (namespace === root || namespace.startsWith(`${root}.`) || namespace.startsWith(`${root}::`)) {
399
+ return namespace;
400
+ }
401
+ }
402
+ return null;
403
+ }
404
+
405
+ /**
406
+ * Does `def` fill a slot of a contract that lives outside the project?
407
+ * Returns null, or { certainty: 'definite'|'possible', via: string[],
408
+ * reason, sites: [{file, relativePath, line}] }.
409
+ * definite - the name is fixed by an out-of-project contract: a Rust impl
410
+ * of an external trait, an explicit override marker with no project
411
+ * ancestor declaring the member, a language protocol member (Python
412
+ * dunders), a universal-supertype member (Java toString/equals/...).
413
+ * possible - the owning class declares out-of-project supertypes and the
414
+ * member is public by shape, so it may implement or override one of
415
+ * their (invisible) members.
416
+ */
417
+ function externalContractOf(index, def) {
418
+ // A declaration inside a standard-library namespace (fix #390: a C#
419
+ // polyfill `namespace System.Diagnostics.CodeAnalysis { enum
420
+ // DynamicallyAccessedMemberTypes }`, a C++ `namespace std { template<>
421
+ // struct hash<X> }`) stands in for, or specializes, the platform's
422
+ // declaration of that full name: where the platform provides it, every
423
+ // reference binds the platform's, so the name cannot change.
424
+ const standardNamespace = def && standardNamespaceOf(index, def);
425
+ if (standardNamespace) {
426
+ return {
427
+ certainty: 'definite',
428
+ via: [`${standardNamespace}`],
429
+ reason: 'standard-namespace-declaration',
430
+ sites: [{ file: def.file, relativePath: def.relativePath || _rel(index, def.file),
431
+ line: def.nameLine || def.startLine }],
432
+ };
433
+ }
434
+ // A callable a macro invocation defines around its body (fix #391:
435
+ // `TEST(Suite, Name) { ... }`): its name comes from the expansion and is
436
+ // not written in the source, so there is no declaration token to edit.
437
+ // A member a member-list invocation declares under a name no argument
438
+ // spells (`Get##name##String`, fix #396) has no token to edit either.
439
+ if (def && def.generatedByMacro && !NON_CALLABLE_TYPES.has(def.type) &&
440
+ (!def.className || def.generatedByMacro.unspelled)) {
441
+ return {
442
+ certainty: 'definite',
443
+ via: [String(def.generatedByMacro.name || 'macro')],
444
+ reason: 'macro-generated-name',
445
+ sites: [{ file: def.file, relativePath: def.relativePath || _rel(index, def.file),
446
+ line: def.nameLine || def.startLine }],
447
+ };
448
+ }
449
+ // A "callable" spelled like a function-like macro in effect where it is
450
+ // declared (fix #401: `T field GUARDED_BY(mu);` read as a member
451
+ // `GUARDED_BY` returning T, in a language whose macros use call syntax):
452
+ // the declaration is the macro's invocation, so there is no function of
453
+ // that name to rename.
454
+ if (def && !NON_CALLABLE_TYPES.has(def.type) && def.file &&
455
+ langTraits(index.files.get(def.file)?.language)?.macroInvocationSyntax === 'call') {
456
+ const macros = (index.symbols.get(def.name) || []).filter(d => d.type === 'macro' && Array.isArray(d.ppParams));
457
+ if (macros.length > 0) {
458
+ const visible = require('./cpp-scope').includeClosure(index, def.file);
459
+ const inEffect = macros.filter(macro => visible.has(macro.file) &&
460
+ (macro.file !== def.file || macro.startLine < (def.nameLine || def.startLine)));
461
+ if (inEffect.length > 0) {
462
+ return {
463
+ certainty: 'definite',
464
+ via: [`${inEffect[0].relativePath || _rel(index, inEffect[0].file)}:${inEffect[0].startLine}`],
465
+ reason: 'macro-invocation',
466
+ sites: [{ file: def.file, relativePath: def.relativePath || _rel(index, def.file),
467
+ line: def.nameLine || def.startLine }],
468
+ };
469
+ }
470
+ }
471
+ }
472
+ // A compiler-required type (fix #380): its full name is fixed by the
473
+ // language, so renaming it withdraws the feature's lowering target.
474
+ if (def && !def.className && CLASS_KINDS.has(def.type) && protocolTypeOf(index, def)) {
475
+ return {
476
+ certainty: 'definite',
477
+ via: [`${def.namespace ? `${def.namespace}.` : ''}${def.name}`],
478
+ reason: 'compiler-protocol-type',
479
+ sites: [{ file: def.file, relativePath: def.relativePath || _rel(index, def.file),
480
+ line: def.nameLine || def.startLine }],
481
+ };
482
+ }
483
+ if (!def || !def.className) return null;
484
+ const defSite = {
485
+ file: def.file,
486
+ relativePath: def.relativePath || _rel(index, def.file),
487
+ line: def.nameLine || def.startLine,
488
+ };
489
+ // A field the runtime reads by name (Java `serialVersionUID`, fix #389).
490
+ if (NON_CALLABLE_TYPES.has(def.type)) {
491
+ const certainty = protocolMemberCertainty(index, def);
492
+ if (!certainty) return null;
493
+ const lang = index.files.get(def.file)?.language;
494
+ return {
495
+ certainty,
496
+ via: certainty === 'definite' ? [`${lang} data model`] : ownerExternalSupertypes(index, def),
497
+ reason: certainty === 'definite' ? 'language-protocol-member' : 'possible-protocol-member',
498
+ sites: [defSite],
499
+ };
500
+ }
501
+ const lang = index.files.get(def.file)?.language;
502
+ const traits = langTraits(lang) || {};
503
+ if (lang === 'go') return null;
504
+
505
+ // Rust: `impl Trait for X` members carry traitName.
506
+ if (def.traitName) {
507
+ // A trait name bound to a project trait under one configuration and
508
+ // to an external trait under another (`#[cfg(feature = "std")] use
509
+ // std::error::Error as StdError;` beside a cfg'd `trait StdError`)
510
+ // is fixed by the external contract in that configuration (fix #376).
511
+ const scoped = rustTraitResolution(index, def.traitName, def.file, 0, def.startLine);
512
+ if (scoped?.external && scoped.project.length > 0) {
513
+ return {
514
+ certainty: 'definite',
515
+ via: [String(def.traitName).replace(/\s+/g, ' ').trim()],
516
+ reason: 'implements-cfg-external-trait',
517
+ sites: [defSite, ...scoped.project.map(t => ({
518
+ file: t.file,
519
+ relativePath: t.relativePath || _rel(index, t.file),
520
+ line: t.startLine,
521
+ }))],
522
+ };
523
+ }
524
+ if (rustProjectTraits(index, def.traitName, def.file, def.startLine).length > 0) return null;
525
+ return {
526
+ certainty: 'definite',
527
+ via: [String(def.traitName).replace(/\s+/g, ' ').trim()],
528
+ reason: 'implements-external-trait',
529
+ sites: [defSite],
530
+ };
531
+ }
532
+ const protocolCertainty = protocolMemberCertainty(index, def);
533
+ if (protocolCertainty === 'definite') {
534
+ return {
535
+ certainty: 'definite',
536
+ via: [`${lang} data model`],
537
+ reason: 'language-protocol-member',
538
+ sites: [defSite],
539
+ };
540
+ }
541
+ if (protocolCertainty === 'possible') {
542
+ // The owner leaves the project through a supertype that may carry
543
+ // the protocol (fix #389): edits stay, the definition is reviewed.
544
+ return {
545
+ certainty: 'possible',
546
+ via: ownerExternalSupertypes(index, def),
547
+ reason: 'possible-protocol-member',
548
+ sites: [defSite],
549
+ };
550
+ }
551
+ const classDefs = ownerClassDefs(index, def);
552
+ const classSites = classDefs.map(cd => ({
553
+ file: cd.file,
554
+ relativePath: cd.relativePath || _rel(index, cd.file),
555
+ line: cd.startLine,
556
+ }));
557
+ const { external, projectDefiner } = heritageOf(index, classDefs, def.name);
558
+ if (projectDefiner) return null; // a project contract: plan's slot closure owns it
559
+ const rootMembers = traits.universalSupertypeMembers;
560
+ const arity = _arity(def);
561
+ if (rootMembers && Object.prototype.hasOwnProperty.call(rootMembers, def.name) &&
562
+ (arity == null || arity === rootMembers[def.name])) {
563
+ return {
564
+ certainty: 'definite',
565
+ via: [traits.universalSupertype || 'Object'],
566
+ reason: 'overrides-universal-supertype',
567
+ sites: [defSite],
568
+ };
569
+ }
570
+ if (isOverrideMarked(def)) {
571
+ return {
572
+ certainty: 'definite',
573
+ via: external.length > 0 ? external : [traits.universalSupertype || 'supertype'],
574
+ reason: 'overrides-external-member',
575
+ sites: [defSite, ...classSites],
576
+ };
577
+ }
578
+ // TS typed object-literal members: `const h: Handler = { handle() {} }`.
579
+ if (def.registryContainerType && !def.className) return null;
580
+ if (external.length === 0) return null;
581
+ // Only a member declared INSIDE the class body is constrained by the
582
+ // class's supertypes: Rust inherent methods live in separate `impl X`
583
+ // blocks and never implement a trait the struct also implements.
584
+ if (!memberInClassBody(index, def)) return null;
585
+ // Contract-satisfiable shape: private and static members never
586
+ // implement or override an instance contract member. Explicit-visibility
587
+ // languages additionally reject non-public interface implementations
588
+ // (the #270 compiler rule); members without a recorded access modifier
589
+ // stay candidates.
590
+ const mods = def.modifiers || [];
591
+ const hidden = mods.includes('private') || mods.includes('static') ||
592
+ String(def.name).startsWith('#') ||
593
+ (typeof traits.privateMemberName === 'function'
594
+ ? traits.privateMemberName(def.name)
595
+ : traits.implicitlyPublicMembers && String(def.name).startsWith('_'));
596
+ if (hidden) return null;
597
+ return {
598
+ certainty: 'possible',
599
+ via: external,
600
+ reason: 'may-implement-external-member',
601
+ sites: [defSite, ...classSites],
602
+ };
603
+ }
604
+
605
+ /**
606
+ * External contract membership of a TS/JS typed object-literal member
607
+ * (`const h: ext.Handler = { handle() {} }`), or the project interface the
608
+ * literal is declared against. Returns { external } | { projectInterface }.
609
+ */
610
+ function typedLiteralContract(index, def) {
611
+ if (!def?.registryContainerType || def.className) return null;
612
+ const bare = _bareTypeName(def.registryContainerType);
613
+ if (!bare || /^(any|unknown|object|Record|Partial|Readonly)$/.test(bare)) return null;
614
+ const projectDefs = _projectTypeDefs(index, bare);
615
+ if (projectDefs.length > 0) {
616
+ const file = index._resolveClassFile(bare, def.file);
617
+ return { projectInterface: { name: bare, file } };
618
+ }
619
+ return {
620
+ external: {
621
+ certainty: 'possible',
622
+ via: [def.registryContainerType],
623
+ reason: 'typed-literal-external-member',
624
+ sites: [{
625
+ file: def.file,
626
+ relativePath: def.relativePath || _rel(index, def.file),
627
+ line: def.nameLine || def.startLine,
628
+ }],
629
+ },
630
+ };
631
+ }
632
+
633
+ /** Typed object-literal members declared against a project interface. */
634
+ function typedLiteralMembers(index, name, interfaceName, interfaceFile) {
635
+ return (index.symbols.get(name) || []).filter(member =>
636
+ member.registryContainerType && !member.className &&
637
+ _bareTypeName(member.registryContainerType) === interfaceName &&
638
+ index._resolveClassFile(interfaceName, member.file) === interfaceFile);
639
+ }
640
+
641
+ // ---------------------------------------------------------------------------
642
+ // Hierarchy slot closure (fix #376)
643
+ // ---------------------------------------------------------------------------
644
+
645
+ const SLOT_SELF_PARAMS = new Set(['self', '&self', '&mut self', 'mut self', 'this', 'cls']);
646
+
647
+ /** A parameter's written type; an unnamed parameter's name is its type (fix #393). */
648
+ function _paramTypeText(param) {
649
+ return param?.type || (param?.unnamed ? param.name : '') || '';
650
+ }
651
+
652
+ /** Parameter-type spelling of a member, receiver params excluded. */
653
+ function _slotSignature(def) {
654
+ if (!Array.isArray(def.paramsStructured)) return null;
655
+ return def.paramsStructured
656
+ .filter(p => !p?.extensionReceiver &&
657
+ !SLOT_SELF_PARAMS.has(String(p?.name || '').trim()))
658
+ .map(p => String(_paramTypeText(p))
659
+ .replace(/@[\w.]+(?:\([^)]*\))?/g, '')
660
+ .replace(/\b(?:final|const|in|ref|out|params|readonly)\b/g, '')
661
+ .replace(/\s+/g, ''))
662
+ .join(',');
663
+ }
664
+
665
+ function _slotTypeName(raw) {
666
+ return String(raw || '').replace(/<.*$/s, '').replace(/\s+/g, '')
667
+ .split('::').pop().split('.').pop();
668
+ }
669
+
670
+ /** Type parameter names a definition (or its owner) declares (`<T, K extends X>`). */
671
+ function _typeParams(...defs) {
672
+ const out = new Set();
673
+ for (const d of defs) {
674
+ const generics = String(d?.generics || '').replace(/^<|>$/g, '');
675
+ for (const part of splitParentList(generics)) {
676
+ const name = part.trim().split(/[\s:]/)[0];
677
+ if (name) out.add(name);
678
+ }
679
+ }
680
+ return out;
681
+ }
682
+
683
+ /**
684
+ * Could `member` override (occupy the same slot as) `pin` by parameter
685
+ * types? Types compare by their terminal name; a type parameter of either
686
+ * side's method or class matches anything (`m(T)` overridden as
687
+ * `m(String)`).
688
+ */
689
+ function _slotParamsCompatible(index, member, pin) {
690
+ const params = d => (d.paramsStructured || []).filter(p => !p?.extensionReceiver &&
691
+ !SLOT_SELF_PARAMS.has(String(p?.name || '').trim()));
692
+ const a = params(member);
693
+ const b = params(pin);
694
+ if (a.length !== b.length) return false;
695
+ const owners = d => (d.className ? (index.symbols.get(d.className) || [])
696
+ .filter(c => c.file === d.file && CLASS_KINDS.has(c.type)) : []);
697
+ const variables = _typeParams(member, pin, ...owners(member), ...owners(pin));
698
+ for (let i = 0; i < a.length; i++) {
699
+ const ta = _slotTypeName(_paramTypeText(a[i]));
700
+ const tb = _slotTypeName(_paramTypeText(b[i]));
701
+ if (!ta || !tb || ta === tb) continue;
702
+ if (variables.has(ta) || variables.has(tb)) continue;
703
+ if (/^[A-Z]$/.test(ta) || /^[A-Z]$/.test(tb)) continue;
704
+ return false;
705
+ }
706
+ return true;
707
+ }
708
+
709
+ // ---------------------------------------------------------------------------
710
+ // Generic base clauses as type-argument substitutions (fix #390)
711
+ // ---------------------------------------------------------------------------
712
+
713
+ /** Split on commas outside `<>`, `()` and `[]`. */
714
+ function _splitTypeArgs(text) {
715
+ const out = [];
716
+ let depth = 0;
717
+ let start = 0;
718
+ const value = String(text || '');
719
+ for (let i = 0; i < value.length; i++) {
720
+ const ch = value[i];
721
+ if (ch === '<' || ch === '(' || ch === '[') depth++;
722
+ else if (ch === '>' || ch === ')' || ch === ']') depth--;
723
+ else if (ch === ',' && depth === 0) {
724
+ out.push(value.slice(start, i).trim());
725
+ start = i + 1;
726
+ }
727
+ }
728
+ out.push(value.slice(start).trim());
729
+ return out.filter(Boolean);
730
+ }
731
+
732
+ const TYPE_PARAM_PREFIXES = new Set(['in', 'out', 'const', 'typename', 'class', 'struct']);
733
+
734
+ /**
735
+ * Ordered type parameter names of a declaration (`<S, R>`, `<T extends X>`,
736
+ * `<in T>`), [] when it declares none, null when its record cannot say (a
737
+ * C# generic type without recorded names).
738
+ */
739
+ function _orderedTypeParams(def) {
740
+ const raw = String(def?.generics || '').trim().replace(/^<|>$/g, '');
741
+ if (!raw) return def?.typeArity ? null : [];
742
+ return _splitTypeArgs(raw).map(part => {
743
+ const tokens = part.replace(/@[\w.]+(?:\([^)]*\))?/g, ' ')
744
+ .split(/[\s:=]+/).filter(Boolean);
745
+ const name = tokens.find(token => !TYPE_PARAM_PREFIXES.has(token)) || '_';
746
+ return name.replace(/\.\.\.$/, '');
747
+ });
748
+ }
749
+
750
+ /**
751
+ * Every distinct argument list `child` writes for `parentName` (a C# class
752
+ * implementing `I<A>` and `I<B>` writes two), [] for a raw base, null when
753
+ * none is recorded.
754
+ */
755
+ function _writtenBaseArgLists(index, childRef, parentName) {
756
+ const { classDefsNamed } = require('./class-identity');
757
+ let decls = childRef.key ? classDefsNamed(index, childRef.name).entries
758
+ .filter(e => e.key === childRef.key).map(e => e.def) : [];
759
+ if (decls.length === 0 && childRef.def) decls = [childRef.def];
760
+ const spelled = new Set();
761
+ for (const decl of decls) {
762
+ const raws = [
763
+ ..._splitTypeArgs(decl.extends),
764
+ ...(Array.isArray(decl.implements) ? decl.implements : _splitTypeArgs(decl.implements)),
765
+ ];
766
+ for (const raw of raws) {
767
+ const text = String(raw).replace(/^(?:(?:public|protected|private|virtual)\s+)+/, '').trim();
768
+ const head = text.replace(/<.*$/s, '').trim();
769
+ if (head.split(/::|\./).pop().trim() !== parentName) continue;
770
+ spelled.add(text.replace(/\s+/g, ' '));
771
+ }
772
+ }
773
+ if (spelled.size === 0) return null;
774
+ const lists = [];
775
+ for (const text of [...spelled].sort()) {
776
+ const open = text.indexOf('<');
777
+ if (open < 0) { lists.push([]); continue; }
778
+ const close = text.lastIndexOf('>');
779
+ if (close <= open) return null;
780
+ lists.push(_splitTypeArgs(text.slice(open + 1, close)));
781
+ }
782
+ return lists;
783
+ }
784
+
785
+ /** Replace unqualified identifiers named in `map` inside a type spelling. */
786
+ function _substituteType(text, map) {
787
+ if (!map || map.size === 0) return String(text || '');
788
+ return String(text || '').replace(/(^|[^\w$.:])([A-Za-z_$][\w$]*)/g,
789
+ (match, before, id) => (map.has(id) ? before + map.get(id) : match));
790
+ }
791
+
792
+ /**
793
+ * The substitution a base clause performs: `parent`'s type parameters to
794
+ * the arguments `child` writes for it (in child's terms), null when either
795
+ * side is unknown or the counts disagree (a raw base).
796
+ */
797
+ function _baseSubstitution(index, childRef, parentRef) {
798
+ const all = _baseSubstitutions(index, childRef, parentRef);
799
+ return all && all.length === 1 ? all[0] : null;
800
+ }
801
+
802
+ /** One substitution per instantiation the child writes (fix #400: a class
803
+ * implementing a generic interface twice fills the slot of each); null
804
+ * when any cannot be read. */
805
+ function _baseSubstitutions(index, childRef, parentRef) {
806
+ const params = _orderedTypeParams(parentRef.def);
807
+ if (!params) return null;
808
+ const lists = _writtenBaseArgLists(index, childRef, parentRef.name);
809
+ if (!lists || lists.length === 0) return null;
810
+ const maps = [];
811
+ for (const args of lists) {
812
+ if (args.length !== params.length) return null;
813
+ const map = new Map();
814
+ params.forEach((param, i) => map.set(param, args[i]));
815
+ maps.push(map);
816
+ }
817
+ return maps;
818
+ }
819
+
820
+ /** A parameter type as dispatch compares it: generic arguments, qualifiers
821
+ * and non-type modifiers removed (`const std::string&` -> `string&`). */
822
+ function _slotTypeKey(raw) {
823
+ let text = String(raw || '')
824
+ .replace(/@[\w.]+(?:\([^)]*\))?/g, '')
825
+ .replace(/\b(?:final|const|volatile|in|ref|out|params|readonly|scoped|typename|struct|class|enum)\b/g, '');
826
+ for (let guard = 0; guard < 8 && /<[^<>]*>/.test(text); guard++) {
827
+ text = text.replace(/<[^<>]*>/g, '');
828
+ }
829
+ return text.replace(/(?:[A-Za-z_$][\w$]*\s*(?:::|\.)\s*)+(?=[A-Za-z_$])/g, '')
830
+ .replace(/\s+/g, '');
831
+ }
832
+
833
+ function _slotParams(def) {
834
+ return (def.paramsStructured || []).filter(p => !p?.extensionReceiver &&
835
+ !SLOT_SELF_PARAMS.has(String(p?.name || '').trim()));
836
+ }
837
+
838
+ /**
839
+ * Does `member` take the slot whose parameter types, in the member's own
840
+ * class terms, are `slotTypes`? Method-level type parameters of either
841
+ * side match any type (`<U> m(U)` overrides `<V> m(V)`).
842
+ */
843
+ function _slotTypesMatch(member, slotTypes, slotMethodParams) {
844
+ const params = _slotParams(member);
845
+ if (params.length !== slotTypes.length) return false;
846
+ const wild = new Set([...(_orderedTypeParams({ generics: member.generics }) || []),
847
+ ...slotMethodParams]);
848
+ for (let i = 0; i < params.length; i++) {
849
+ const a = _slotTypeKey(params[i]?.type);
850
+ const b = _slotTypeKey(slotTypes[i]);
851
+ if (!a || !b || a === b) continue;
852
+ const baseA = a.replace(/[^\w$].*$/s, '');
853
+ const baseB = b.replace(/[^\w$].*$/s, '');
854
+ if (wild.has(baseA) || wild.has(baseB)) continue;
855
+ return false;
856
+ }
857
+ return true;
858
+ }
859
+
860
+ /**
861
+ * Members named `name` that class reference `ref` declares and that occupy
862
+ * the same dispatch slot as `pin`. Languages with overloads by parameter
863
+ * list (hasArityOverloads) require the same arity and the same parameter
864
+ * types. With a `frame` (fix #390) the types compare through the generic
865
+ * base clauses between the two classes: `frame.slotTypes` are the slot's
866
+ * parameter types in `ref`'s terms, or `frame.memberSubst` maps `ref`'s type
867
+ * parameters into the pin's terms. Without one (a base clause that cannot
868
+ * be read), type parameters of either side match anything.
869
+ */
870
+ /** A member declared `private`, `protected` or `internal` (never an
871
+ * interface member's implementation). */
872
+ function _nonPublicMember(member) {
873
+ return (member?.modifiers || []).some(modifier =>
874
+ modifier === 'private' || modifier === 'protected' || modifier === 'internal');
875
+ }
876
+
877
+ /** The interface a C# explicit implementation implements, by name. */
878
+ function _explicitInterfaceName(member) {
879
+ return member?.explicitInterface
880
+ ? String(member.explicitInterface).replace(/<.*$/s, '').split('.').pop().trim() : null;
881
+ }
882
+
883
+ function _slotMembersOf(index, name, ref, pin, overloads, frame = null, slotInterfaces = null) {
884
+ const { ownedBy } = require('./class-identity');
885
+ const refFile = ref.def?.file || null;
886
+ let defs = (index.symbols.get(name) || []).filter(member => {
887
+ if (NON_CALLABLE_TYPES.has(member.type) || member.className !== ref.name) return false;
888
+ const owned = ownedBy(index, member, ref);
889
+ return owned === 'yes' || (owned === 'maybe' && refFile && member.file === refFile);
890
+ });
891
+ // C# explicit interface implementations (fix #395) fill only their
892
+ // interface's slot: `string IValidator.M()` never shares a slot with a
893
+ // `protected virtual M()` of the same class, and where a class
894
+ // implements the pin's interface member explicitly, its other same-name
895
+ // members are not that slot.
896
+ if (defs.some(member => member.explicitInterface)) {
897
+ const interfaces = new Set(slotInterfaces || []);
898
+ if (_explicitInterfaceName(pin)) interfaces.add(_explicitInterfaceName(pin));
899
+ const explicit = defs.filter(member => interfaces.has(_explicitInterfaceName(member)));
900
+ defs = explicit.length > 0 ? explicit : defs.filter(member => !member.explicitInterface);
901
+ }
902
+ if (!overloads || defs.length === 0) return defs;
903
+ const arity = _arity(pin);
904
+ let same = defs.filter(member => arity == null || _arity(member) == null ||
905
+ _arity(member) === arity);
906
+ // Where generic arity is identity (C#), a method overrides or implements
907
+ // only a method with as many type parameters (fix #391: `Write<T>(.., T)`
908
+ // is not the slot of `Write(.., params object[])`).
909
+ if (langTraits(index.files.get(pin.file)?.language)?.genericArityIsIdentity) {
910
+ const methodArity = def => (_orderedTypeParams({ generics: def.generics }) || []).length;
911
+ const pinMethodArity = methodArity(pin);
912
+ same = same.filter(member => methodArity(member) === pinMethodArity);
913
+ }
914
+ if (frame && arity != null) {
915
+ const pinMethodParams = _orderedTypeParams({ generics: pin.generics }) || [];
916
+ if (frame.slotTypes) {
917
+ return same.filter(member => _arity(member) == null ||
918
+ _slotTypesMatch(member, frame.slotTypes, pinMethodParams));
919
+ }
920
+ if (frame.memberSubst) {
921
+ const pinTypes = _slotParams(pin).map(p => p?.type);
922
+ return same.filter(member => {
923
+ if (_arity(member) == null) return true;
924
+ const inPinTerms = { ...member, paramsStructured: _slotParams(member)
925
+ .map(p => ({ ...p, type: _substituteType(p?.type, frame.memberSubst) })) };
926
+ return _slotTypesMatch(inPinTerms, pinTypes, pinMethodParams);
927
+ });
928
+ }
929
+ }
930
+ const signature = _slotSignature(pin);
931
+ const exact = same.filter(member => _slotSignature(member) === signature);
932
+ if (exact.length > 0) return exact;
933
+ same = same.filter(member => _arity(member) == null || arity == null ||
934
+ _slotParamsCompatible(index, member, pin));
935
+ return same;
936
+ }
937
+
938
+ /**
939
+ * The project classes a renamed member's dispatch slot spans: every
940
+ * transitive ancestor (through intermediate classes that do not redeclare
941
+ * the member, declared implements clauses included for body members) that
942
+ * declares the slot, and every transitive descendant of the slot's owners
943
+ * that overrides it. Classes resolve by definition identity (class-identity
944
+ * refs), with the name-level file resolution as the fallback for an
945
+ * ambiguous parent spelling. Ancestor names that cannot be resolved at all
946
+ * are returned in `unresolved` for review.
947
+ *
948
+ * @param {object} index
949
+ * @param {object} def - the pinned member definition
950
+ * @param {object} [startRef] - class reference to start from (typed object
951
+ * literal members start at their declared interface)
952
+ * @returns {{pinRef, ancestors: {ref, members}[], descendants: {ref, members,
953
+ * viaImplements}[], unresolved: string[], external: string[]}|null}
954
+ */
955
+ function hierarchySlotClosure(index, def, startRef = null) {
956
+ const { ownerRefOf, supertypeRefsOf, classDefsNamed } = require('./class-identity');
957
+ const pinRef = startRef || ownerRefOf(index, def);
958
+ if (!pinRef || (!pinRef.key && !pinRef.def)) return null;
959
+ const lang = index.files.get(def.file)?.language;
960
+ const overloads = !!langTraits(lang)?.hasArityOverloads;
961
+ const name = def.name;
962
+ // Only a member declared inside the class body is constrained by the
963
+ // class's implements clause (a Rust inherent method never fills the
964
+ // struct's trait slots); typed literal members start at the interface.
965
+ const followImplements = !!startRef || memberInClassBody(index, def);
966
+ const refKey = ref => ref.key || `?${ref.name}`;
967
+ const resolveLoose = (parent, contextFile) => {
968
+ if (parent.key || parent.def) return parent;
969
+ // Name-level fallback for a spelling identity cannot pin: the class
970
+ // is followed, but the plan reviews it (unresolved).
971
+ if (!unresolved.includes(parent.name)) unresolved.push(parent.name);
972
+ const file = index._resolveClassFile(parent.name, contextFile);
973
+ const entry = file && classDefsNamed(index, parent.name).entries
974
+ .find(e => e.def.file === file);
975
+ return entry ? { name: parent.name, key: entry.key, def: entry.def } : null;
976
+ };
977
+
978
+ const ancestors = [];
979
+ const unresolved = [];
980
+ const external = [];
981
+ const ancestorSeen = new Set([refKey(pinRef)]);
982
+ // Each class carries the substitution of its type parameters into the
983
+ // pin's terms, composed over the generic base clauses walked (fix #390):
984
+ // `Mid<T> : Base<T, bool>` maps Base's S to the pin class's T. null when
985
+ // a base clause on the way cannot be read.
986
+ // Interfaces whose member of this name a class on the upward path
987
+ // implements explicitly (C#).
988
+ const explicitViaOf = (ref, inherited) => {
989
+ const own = (index.symbols.get(name) || []).filter(member => member.className === ref.name &&
990
+ member.explicitInterface).map(_explicitInterfaceName);
991
+ if (own.length === 0) return inherited || null;
992
+ return new Set([...(inherited || []), ...own]);
993
+ };
994
+ const upQueue = [{ ref: pinRef, contextFile: pinRef.def?.file || def.file,
995
+ subst: overloads ? new Map() : null, explicitVia: explicitViaOf(pinRef, null) }];
996
+ while (upQueue.length > 0 && ancestorSeen.size < 256) {
997
+ const current = upQueue.shift();
998
+ for (const parent of supertypeRefsOf(index, current.ref,
999
+ { implements: followImplements })) {
1000
+ if (parent.external) {
1001
+ if (!external.includes(parent.name)) external.push(parent.name);
1002
+ continue;
1003
+ }
1004
+ const resolved = resolveLoose(parent, current.contextFile);
1005
+ if (!resolved) continue;
1006
+ const key = refKey(resolved);
1007
+ if (ancestorSeen.has(key)) continue;
1008
+ ancestorSeen.add(key);
1009
+ let subst = null;
1010
+ if (current.subst && resolved.def) {
1011
+ const step = _baseSubstitution(index, current.ref, resolved);
1012
+ if (step) {
1013
+ subst = new Map();
1014
+ for (const [param, arg] of step) subst.set(param, _substituteType(arg, current.subst));
1015
+ }
1016
+ }
1017
+ // A class on the way that implements this interface's member
1018
+ // explicitly keeps the pin's slot out of the interface (fix
1019
+ // #395, C#): `string IValidator.M()` beside `protected virtual
1020
+ // M()` are two slots.
1021
+ if (!_explicitInterfaceName(def) && current.explicitVia?.has(resolved.name)) continue;
1022
+ // An interface member is implemented by public members only (C#,
1023
+ // Java, TS): a `protected`/`private`/`internal` member is never
1024
+ // its slot (fix #395).
1025
+ if (resolved.def?.type === 'interface' && !_explicitInterfaceName(def) && _nonPublicMember(def)) continue;
1026
+ const members = _slotMembersOf(index, name, resolved, def, overloads,
1027
+ subst ? { memberSubst: subst } : null);
1028
+ if (members.length > 0) ancestors.push({ ref: resolved, members });
1029
+ upQueue.push({ ref: resolved, contextFile: resolved.def?.file || current.contextFile, subst,
1030
+ explicitVia: explicitViaOf(resolved, current.explicitVia) });
1031
+ }
1032
+ }
1033
+
1034
+ // Children spelled their parent with a qualifier the inheritance graph
1035
+ // kept (`extends Outer.Inner<T>`): look those buckets up too.
1036
+ let qualifiedKeys = null;
1037
+ const qualifiedChildKeys = parentName => {
1038
+ if (!qualifiedKeys) {
1039
+ qualifiedKeys = new Map();
1040
+ for (const key of index.extendedByGraph.keys()) {
1041
+ const terminal = String(key).split('::').pop().split('.').pop();
1042
+ if (terminal === key) continue;
1043
+ if (!qualifiedKeys.has(terminal)) qualifiedKeys.set(terminal, []);
1044
+ qualifiedKeys.get(terminal).push(key);
1045
+ }
1046
+ }
1047
+ return [parentName, ...(qualifiedKeys.get(parentName) || [])];
1048
+ };
1049
+ // Descendants of every slot owner: the pin's class and each declaring
1050
+ // ancestor (siblings of the pin override the same slot).
1051
+ const descendants = [];
1052
+ const roots = [pinRef, ...ancestors.map(a => a.ref)];
1053
+ const downSeen = new Set(roots.map(refKey));
1054
+ // Each class carries the slot's parameter types in its own terms, read
1055
+ // through the generic base clauses from the root that declares the slot
1056
+ // (fix #390): below `Fmt : Visitor<TextWriter, bool>` the slot
1057
+ // `Visit(TState, int)` is `Visit(TextWriter, int)`.
1058
+ const slotTypesOf = member => _slotParams(member).map(p => p?.type);
1059
+ // A class whose slot member is an explicit interface implementation
1060
+ // (C#) has no subclass override of it: explicit implementations are not
1061
+ // virtual (fix #395); a subclass re-implementing the interface is found
1062
+ // through the interface.
1063
+ const onlyExplicit = members => members.length > 0 && members.every(m => m.explicitInterface);
1064
+ const interfaceSlot = pinRef.def?.type === 'interface' || !!def.explicitInterface ||
1065
+ ancestors.some(a => a.ref.def?.type === 'interface' && a.members.length > 0);
1066
+ // The interfaces whose member is this slot: their explicit
1067
+ // implementations below are slot members.
1068
+ const slotInterfaces = new Set([
1069
+ ...(pinRef.def?.type === 'interface' ? [pinRef.name] : []),
1070
+ ...ancestors.filter(a => a.ref.def?.type === 'interface' && a.members.length > 0).map(a => a.ref.name),
1071
+ ...(_explicitInterfaceName(def) ? [_explicitInterfaceName(def)] : []),
1072
+ ]);
1073
+ const downQueue = [
1074
+ { ref: pinRef, slotTypes: overloads ? slotTypesOf(def) : null, explicitOnly: !!def.explicitInterface },
1075
+ ...ancestors.map(a => ({ ref: a.ref,
1076
+ slotTypes: overloads && a.members.length === 1 ? slotTypesOf(a.members[0]) : null,
1077
+ explicitOnly: onlyExplicit(a.members) })),
1078
+ ];
1079
+ // The slot's parameter types in each class's own terms (null: unknown).
1080
+ const slotTypesByKey = new Map(downQueue.map(entry => [refKey(entry.ref), entry.slotTypes]));
1081
+ while (downQueue.length > 0 && descendants.length < 5000) {
1082
+ const currentEntry = downQueue.shift();
1083
+ const current = currentEntry.ref;
1084
+ const children = [
1085
+ ...(currentEntry.explicitOnly ? [] : qualifiedChildKeys(current.name).flatMap(key =>
1086
+ index.extendedByGraph.get(key) || []).map(child =>
1087
+ typeof child === 'string' ? { name: child, file: null }
1088
+ : { name: child.name, file: child.file || null })),
1089
+ ...implementedBy(index, current.name),
1090
+ ];
1091
+ for (const child of children) {
1092
+ if (!child.name) continue;
1093
+ const entries = classDefsNamed(index, child.name).entries
1094
+ .filter(e => !child.file || e.def.file === child.file);
1095
+ for (const entry of entries) {
1096
+ if (downSeen.has(entry.key)) continue;
1097
+ const childRef = { name: child.name, key: entry.key, def: entry.def };
1098
+ const link = supertypeRefsOf(index, childRef, { implements: true }).find(p => {
1099
+ if (p.external || _slotTypeName(p.name) !== current.name) return false;
1100
+ if (p.key) return p.key === current.key;
1101
+ return !!current.def?.file &&
1102
+ index._resolveClassFile(current.name, entry.def.file) === current.def.file;
1103
+ });
1104
+ if (!link) continue;
1105
+ downSeen.add(entry.key);
1106
+ let slotTypes = null;
1107
+ let instantiations = null;
1108
+ if (currentEntry.slotTypes && current.def) {
1109
+ const steps = _baseSubstitutions(index, childRef, current);
1110
+ if (steps?.length === 1) {
1111
+ slotTypes = currentEntry.slotTypes.map(type => _substituteType(type, steps[0]));
1112
+ } else if (steps?.length > 1) {
1113
+ instantiations = steps.map(step => currentEntry.slotTypes.map(type => _substituteType(type, step)));
1114
+ }
1115
+ }
1116
+ // A class implementing the generic base more than once fills
1117
+ // the slot once per instantiation (fix #400).
1118
+ let members = instantiations
1119
+ ? [...new Set(instantiations.flatMap(types => _slotMembersOf(index, name, childRef, def,
1120
+ overloads, { slotTypes: types }, slotInterfaces)))]
1121
+ : _slotMembersOf(index, name, childRef, def, overloads,
1122
+ slotTypes ? { slotTypes } : null, slotInterfaces);
1123
+ if (link.viaImplements) members = members.filter(m => memberInClassBody(index, m));
1124
+ // In an interface member's slot a class takes part with a
1125
+ // public member or an explicit implementation only (fix
1126
+ // #395): a protected override below it overrides another
1127
+ // slot.
1128
+ if (interfaceSlot && childRef.def?.type !== 'interface') {
1129
+ members = members.filter(m => !!m.explicitInterface || !_nonPublicMember(m));
1130
+ }
1131
+ descendants.push({ ref: childRef, members, viaImplements: !!link.viaImplements });
1132
+ downQueue.push({ ref: childRef, slotTypes, explicitOnly: onlyExplicit(members) });
1133
+ slotTypesByKey.set(entry.key, slotTypes);
1134
+ }
1135
+ }
1136
+ }
1137
+ if (startRef) {
1138
+ const own = _slotMembersOf(index, name, startRef, def, overloads);
1139
+ if (own.length > 0) ancestors.unshift({ ref: startRef, members: own });
1140
+ }
1141
+ return { pinRef, ancestors, descendants, unresolved, external, slotTypesByKey };
1142
+ }
1143
+
1144
+ /**
1145
+ * Members of anonymous class bodies that fill the slot (fix #390): a Java
1146
+ * `new Listener<T>(..) { void run(..) {} }` or a JS/TS class expression
1147
+ * `class extends Base { m() {} }` subclasses the created or extended type
1148
+ * without being an indexed class, so its override of a renamed slot member
1149
+ * would be left behind (javac: "does not override"). Scans only the files
1150
+ * and lines where the name occurs. Returns [{ file, line, column, verdict:
1151
+ * 'edit' | 'review', reason }] with parser columns of the member's name.
1152
+ */
1153
+ function anonymousSlotMembers(index, name, perFile, closure, pin) {
1154
+ const { resolveClassRef } = require('./class-identity');
1155
+ if (!closure || !perFile) return [];
1156
+ const slotKeys = new Set([closure.pinRef, ...closure.ancestors.map(a => a.ref),
1157
+ ...closure.descendants.map(d => d.ref)].filter(ref => ref?.key).map(ref => ref.key));
1158
+ if (slotKeys.size === 0) return [];
1159
+ const overloads = !!langTraits(index.files.get(pin.file)?.language)?.hasArityOverloads;
1160
+ const pinMethodParams = _orderedTypeParams({ generics: pin.generics }) || [];
1161
+ const out = [];
1162
+ for (const [file, lineNos] of perFile) {
1163
+ const fileEntry = index.files.get(file);
1164
+ if (!fileEntry || !lineNos || lineNos.size === 0) continue;
1165
+ const language = fileEntry.language;
1166
+ const javaLike = language === 'java';
1167
+ const classExpressionLanguage = ['javascript', 'typescript', 'tsx'].includes(language);
1168
+ if (!javaLike && !classExpressionLanguage) continue;
1169
+ let content;
1170
+ try { content = index._readFile(file); } catch { continue; }
1171
+ const tree = index._getParsedTree?.(file, content, language);
1172
+ if (!tree) continue;
1173
+ const namespace = (fileEntry.symbols || []).find(s => s.namespace)?.namespace || null;
1174
+ const lines = [...lineNos].sort((a, b) => a - b);
1175
+ const fileLines = index._getFileLines(file);
1176
+ const seen = new Set();
1177
+ for (const line of lines) {
1178
+ const text = fileLines[line - 1] || '';
1179
+ for (let at = text.indexOf(name); at >= 0; at = text.indexOf(name, at + 1)) {
1180
+ const node = tree.rootNode.descendantForPosition({ row: line - 1, column: at });
1181
+ if (!node || node.text !== name) continue;
1182
+ const method = node.parent;
1183
+ const methodType = javaLike ? 'method_declaration' : 'method_definition';
1184
+ if (!method || method.type !== methodType) continue;
1185
+ const nameNode = method.childForFieldName('name');
1186
+ if (!nameNode || nameNode.startIndex !== node.startIndex) continue;
1187
+ const body = method.parent;
1188
+ const owner = body?.parent;
1189
+ if (body?.type !== 'class_body' || !owner) continue;
1190
+ let typeText;
1191
+ if (javaLike && owner.type === 'object_creation_expression') {
1192
+ typeText = owner.childForFieldName('type')?.text || null;
1193
+ } else if (javaLike && owner.type === 'enum_constant') {
1194
+ // An enum constant's body subclasses its enum (fix #392).
1195
+ typeText = owner.parent?.parent?.type === 'enum_declaration'
1196
+ ? owner.parent.parent.childForFieldName('name')?.text || null : null;
1197
+ } else if (classExpressionLanguage && owner.type === 'class') {
1198
+ const heritage = owner.namedChildren.find(child => child.type === 'class_heritage');
1199
+ const extendsClause = heritage?.namedChildren.find(child => child.type === 'extends_clause') || heritage;
1200
+ const value = extendsClause?.childForFieldName?.('value') || extendsClause?.namedChildren?.[0];
1201
+ typeText = value ? `${value.text}${extendsClause.namedChildren.find(child => child.type === 'type_arguments')?.text || ''}` : null;
1202
+ } else continue;
1203
+ if (!typeText) continue;
1204
+ const key = `${file}:${nameNode.startIndex}`;
1205
+ if (seen.has(key)) continue;
1206
+ seen.add(key);
1207
+ const head = typeText.replace(/<.*$/s, '').trim();
1208
+ const terminal = head.split(/::|\./).pop().trim();
1209
+ const ref = resolveClassRef(index, terminal, file, { namespace, line });
1210
+ const site = { file, line: nameNode.startPosition.row + 1, column: nameNode.startPosition.column };
1211
+ if (ref.external) continue;
1212
+ if (!ref.key) {
1213
+ if (classDefsIncludeSlotName(index, terminal, slotKeys)) {
1214
+ out.push({ ...site, verdict: 'review', reason: 'anonymous-class-type-unresolved' });
1215
+ }
1216
+ continue;
1217
+ }
1218
+ if (!slotKeys.has(ref.key)) continue;
1219
+ if (overloads) {
1220
+ const params = (method.childForFieldName('parameters')?.namedChildren || [])
1221
+ .filter(p => p.type === 'formal_parameter' || p.type === 'spread_parameter')
1222
+ .map(p => ({ name: p.childForFieldName('name')?.text || '',
1223
+ type: `${p.childForFieldName('type')?.text || p.namedChildren[0]?.text || ''}${p.type === 'spread_parameter' ? '...' : ''}` }));
1224
+ const ownerTypes = closure.slotTypesByKey?.get(ref.key);
1225
+ let slotTypes = null;
1226
+ if (ownerTypes) {
1227
+ const typeParams = _orderedTypeParams(ref.def);
1228
+ const open = typeText.indexOf('<');
1229
+ const args = open >= 0 ? _splitTypeArgs(typeText.slice(open + 1, typeText.lastIndexOf('>'))) : [];
1230
+ if (!(typeParams && typeParams.length > 0) && args.length === 0) {
1231
+ slotTypes = ownerTypes; // a non-generic type: nothing to substitute
1232
+ } else if (typeParams && typeParams.length === args.length) {
1233
+ const map = new Map();
1234
+ typeParams.forEach((param, i) => map.set(param, args[i]));
1235
+ slotTypes = ownerTypes.map(type => _substituteType(type, map));
1236
+ } else if (typeParams && typeParams.length > 0 && args.length === 0 && open >= 0) {
1237
+ slotTypes = null; // diamond `new X<>() {..}`: arguments inferred
1238
+ }
1239
+ }
1240
+ const member = { paramsStructured: params, generics: null };
1241
+ const fits = slotTypes
1242
+ ? _slotTypesMatch(member, slotTypes, pinMethodParams)
1243
+ : params.length === _slotParams(pin).length;
1244
+ if (!fits) continue;
1245
+ if (!slotTypes) {
1246
+ out.push({ ...site, verdict: 'review', reason: 'anonymous-class-signature-unresolved' });
1247
+ continue;
1248
+ }
1249
+ }
1250
+ out.push({ ...site, verdict: 'edit' });
1251
+ }
1252
+ }
1253
+ }
1254
+ return out;
1255
+ }
1256
+
1257
+ function classDefsIncludeSlotName(index, name, slotKeys) {
1258
+ const { classDefsNamed } = require('./class-identity');
1259
+ return classDefsNamed(index, name).entries.some(entry => slotKeys.has(entry.key));
1260
+ }
1261
+
1262
+ // ---------------------------------------------------------------------------
1263
+ // Go structural satisfaction
1264
+ // ---------------------------------------------------------------------------
1265
+
1266
+ const GO_BUILTIN_TYPES = new Set([
1267
+ 'bool', 'byte', 'complex64', 'complex128', 'error', 'float32', 'float64',
1268
+ 'int', 'int8', 'int16', 'int32', 'int64', 'rune', 'string', 'uint',
1269
+ 'uint8', 'uint16', 'uint32', 'uint64', 'uintptr', 'any', 'comparable',
1270
+ ]);
1271
+
1272
+ /** Split a Go signature list on commas outside nested type syntax. */
1273
+ function splitGoSignatureList(raw) {
1274
+ const text = String(raw || '').trim().replace(/^\((.*)\)$/s, '$1');
1275
+ if (!text) return [];
1276
+ const out = [];
1277
+ let start = 0;
1278
+ let depth = 0;
1279
+ for (let i = 0; i < text.length; i++) {
1280
+ const ch = text[i];
1281
+ if (ch === '(' || ch === '[' || ch === '{') depth++;
1282
+ else if (ch === ')' || ch === ']' || ch === '}') depth = Math.max(0, depth - 1);
1283
+ else if (ch === ',' && depth === 0) {
1284
+ out.push(text.slice(start, i).trim());
1285
+ start = i + 1;
1286
+ }
1287
+ }
1288
+ out.push(text.slice(start).trim());
1289
+ return out.filter(Boolean);
1290
+ }
1291
+
1292
+ const GO_UNNAMED_TYPE_PREFIX = /^(?:\*|\[|map\[|chan(?:<-)?\s|<-chan\s|func\s*\(|interface\s*\{|struct\s*\{|\.\.\.)/;
1293
+
1294
+ /** One raw Go parameter/result declaration -> its type spelling. */
1295
+ function goDeclarationType(raw) {
1296
+ let value = String(raw || '').trim();
1297
+ if (!value) return null;
1298
+ if (!GO_UNNAMED_TYPE_PREFIX.test(value)) {
1299
+ const named = value.match(/^[A-Za-z_][A-Za-z0-9_]*\s+(.+)$/s);
1300
+ if (named) value = named[1].trim();
1301
+ }
1302
+ return value.replace(/\s+/g, '');
1303
+ }
1304
+
1305
+ function _goKey(dir, name) { return `${dir}\0${name}`; }
1306
+
1307
+ function _goState(index) {
1308
+ const state = _state(index);
1309
+ if (state.go) return state.go;
1310
+ const go = {
1311
+ // key -> declarations (several when build-constrained files in one
1312
+ // package declare alternative variants of the same type: one
1313
+ // identity, every variant renamed together)
1314
+ types: new Map(),
1315
+ ifaces: new Map(),
1316
+ ambiguous: new Set(), // keys declared both as interface and as named type
1317
+ methods: new Map(), // owner key -> concrete method defs
1318
+ members: new Map(), // iface key -> method element defs
1319
+ fields: new Map(), // owner key -> field defs
1320
+ methodSets: new Map(),
1321
+ requirements: new Map(),
1322
+ fingerprints: new WeakMap(),
1323
+ typesByMethod: null,
1324
+ ifacesByMethod: null,
1325
+ conversions: null,
1326
+ };
1327
+ const push = (map, key, value) => {
1328
+ if (!map.has(key)) map.set(key, []);
1329
+ map.get(key).push(value);
1330
+ };
1331
+ for (const defs of index.symbols.values()) {
1332
+ for (const def of defs) {
1333
+ if (!def.file || index.files.get(def.file)?.language !== 'go') continue;
1334
+ const dir = path.dirname(def.file);
1335
+ if (def.type === 'interface' || def.type === 'struct' || def.type === 'type') {
1336
+ const key = _goKey(dir, def.name);
1337
+ const own = def.type === 'interface' ? go.ifaces : go.types;
1338
+ const other = def.type === 'interface' ? go.types : go.ifaces;
1339
+ if (other.has(key)) go.ambiguous.add(key);
1340
+ push(own, key, def);
1341
+ } else if (def.className && def.type === 'method') {
1342
+ push(go.members, _goKey(dir, def.className), def);
1343
+ } else if (def.className && def.type === 'field') {
1344
+ push(go.fields, _goKey(dir, def.className), def);
1345
+ } else if (def.className && !NON_CALLABLE_TYPES.has(def.type)) {
1346
+ push(go.methods, _goKey(dir, def.className), def);
1347
+ }
1348
+ }
1349
+ }
1350
+ state.go = go;
1351
+ return go;
1352
+ }
1353
+
1354
+ /** Package directory a Go qualifier names in `file`: dir, `ext:<path>`, or null. */
1355
+ function _goQualifierDir(index, file, qualifier) {
1356
+ const state = _state(index);
1357
+ const memoKey = `${file}\0${qualifier}`;
1358
+ if (state.goQualifier.has(memoKey)) return state.goQualifier.get(memoKey);
1359
+ const entry = index.files.get(file);
1360
+ let result = null;
1361
+ const binding = (entry?.importBindings || []).find(b =>
1362
+ (b.alias || b.name) === qualifier);
1363
+ if (binding?.module) {
1364
+ // One resolution per importing directory and import path: every
1365
+ // file of a Go package resolves an import path the same way.
1366
+ if (!state.goImportDir) state.goImportDir = new Map();
1367
+ const dirKey = `${path.dirname(file)}\0${binding.module}`;
1368
+ if (state.goImportDir.has(dirKey)) {
1369
+ result = state.goImportDir.get(dirKey);
1370
+ } else {
1371
+ const { resolveImport } = require('./imports');
1372
+ let resolved;
1373
+ try {
1374
+ resolved = resolveImport(binding.module, file, { language: 'go', root: index.root });
1375
+ } catch { resolved = null; }
1376
+ result = resolved ? path.dirname(resolved) : `ext:${binding.module}`;
1377
+ state.goImportDir.set(dirKey, result);
1378
+ }
1379
+ }
1380
+ state.goQualifier.set(memoKey, result);
1381
+ return result;
1382
+ }
1383
+
1384
+ /**
1385
+ * Resolve a Go type spelling (`T`, `*T`, `pkg.T`) seen in `file` to
1386
+ * { kind: 'iface'|'type', key } | { kind: 'external', name } |
1387
+ * { kind: 'builtin', name } | { kind: 'other' } | null (unknown).
1388
+ */
1389
+ function _goResolveType(index, file, spelled) {
1390
+ const go = _goState(index);
1391
+ let text = String(spelled || '').replace(/\s+/g, '');
1392
+ const pointer = text.startsWith('*');
1393
+ text = text.replace(/^\*+/, '').replace(/\[.*\]$/s, '');
1394
+ if (text === 'interface{}' || text === 'any') return { kind: 'builtin', name: 'any', pointer };
1395
+ const match = text.match(/^(?:([A-Za-z_]\w*)\.)?([A-Za-z_]\w*)$/);
1396
+ if (!match) return { kind: 'other' };
1397
+ const [, qualifier, name] = match;
1398
+ let dir;
1399
+ if (qualifier) {
1400
+ dir = _goQualifierDir(index, file, qualifier);
1401
+ if (!dir) return null;
1402
+ if (dir.startsWith('ext:')) return { kind: 'external', name: `${qualifier}.${name}`, pointer };
1403
+ } else {
1404
+ if (GO_BUILTIN_TYPES.has(name)) return { kind: 'builtin', name, pointer };
1405
+ dir = path.dirname(file);
1406
+ }
1407
+ const key = _goKey(dir, name);
1408
+ if (go.ambiguous.has(key)) return null;
1409
+ if (go.ifaces.has(key)) return { kind: 'iface', key, pointer };
1410
+ if (go.types.has(key)) return { kind: 'type', key, pointer };
1411
+ return qualifier ? { kind: 'external', name: `${qualifier}.${name}`, pointer } : null;
1412
+ }
1413
+
1414
+ /** Package-canonical type text: project types become `<dir>::Name`. */
1415
+ function _goCanonicalType(index, file, text) {
1416
+ const go = _goState(index);
1417
+ const dir = path.dirname(file);
1418
+ return String(text || '').replace(/\s+/g, '').replace(
1419
+ /([A-Za-z_]\w*)(?:\.([A-Za-z_]\w*))?/g, (whole, first, second) => {
1420
+ if (second) {
1421
+ const qdir = _goQualifierDir(index, file, first);
1422
+ if (!qdir) return whole;
1423
+ return qdir.startsWith('ext:') ? `${qdir.slice(4)}.${second}` : `${qdir}::${second}`;
1424
+ }
1425
+ if (GO_BUILTIN_TYPES.has(first)) return first;
1426
+ const key = _goKey(dir, first);
1427
+ return (go.types.has(key) || go.ifaces.has(key)) ? `${dir}::${first}` : first;
1428
+ });
1429
+ }
1430
+
1431
+ /** Compiler-shaped method fingerprint: canonical parameter + result types. */
1432
+ function goMethodFingerprint(index, def) {
1433
+ const go = _goState(index);
1434
+ if (go.fingerprints.has(def)) return go.fingerprints.get(def);
1435
+ let fingerprint = null;
1436
+ let params;
1437
+ if (Array.isArray(def.paramsStructured) && def.paramsStructured.length > 0) {
1438
+ params = def.paramsStructured.map(param =>
1439
+ goDeclarationType(param.type || (param.unnamed ? param.name : null)));
1440
+ } else if (def.params === '' || def.params == null) {
1441
+ params = [];
1442
+ } else {
1443
+ params = splitGoSignatureList(def.params).map(goDeclarationType);
1444
+ }
1445
+ const results = def.returnType
1446
+ ? splitGoSignatureList(def.returnType).map(goDeclarationType) : [];
1447
+ if (!params.some(type => !type) && !results.some(type => !type)) {
1448
+ fingerprint = `${params.map(t => _goCanonicalType(index, def.file, t)).join(',')}->` +
1449
+ `${results.map(t => _goCanonicalType(index, def.file, t)).join(',')}`;
1450
+ }
1451
+ go.fingerprints.set(def, fingerprint);
1452
+ return fingerprint;
1453
+ }
1454
+
1455
+ /** Record one method under `name`, merging build-constrained variants. */
1456
+ function _goAddMethod(methods, name, fingerprint, defs) {
1457
+ const previous = methods.get(name);
1458
+ if (!previous) {
1459
+ methods.set(name, { fingerprint, def: defs[0], defs: [...defs] });
1460
+ return;
1461
+ }
1462
+ for (const d of defs) if (!previous.defs.includes(d)) previous.defs.push(d);
1463
+ if (previous.fingerprint !== fingerprint) previous.fingerprint = null;
1464
+ }
1465
+
1466
+ function _goInRange(member, typeDef) {
1467
+ return member.file === typeDef.file && member.startLine >= typeDef.startLine &&
1468
+ member.startLine <= typeDef.endLine;
1469
+ }
1470
+
1471
+ /**
1472
+ * Requirement set of a project interface: name -> {fingerprint, def}. `open`
1473
+ * when an embedded interface is external/unresolved (its requirements are
1474
+ * invisible); `openVia` names them.
1475
+ */
1476
+ function _goRequirements(index, key, visiting = new Set()) {
1477
+ const go = _goState(index);
1478
+ if (go.requirements.has(key)) return go.requirements.get(key);
1479
+ const variants = go.ifaces.get(key);
1480
+ if (!variants || visiting.has(key)) return null;
1481
+ visiting.add(key);
1482
+ const result = { methods: new Map(), open: false, openVia: [], conflicts: new Set() };
1483
+ if (variants.some(iface => iface.generics)) {
1484
+ result.open = true; result.openVia.push('type parameters');
1485
+ }
1486
+ for (const member of go.members.get(key) || []) {
1487
+ if (!variants.some(iface => _goInRange(member, iface))) continue;
1488
+ _goAddMethod(result.methods, member.name, goMethodFingerprint(index, member), [member]);
1489
+ }
1490
+ for (const field of go.fields.get(key) || []) {
1491
+ const iface = variants.find(variant => _goInRange(field, variant));
1492
+ if (!field.embedded || !iface) continue;
1493
+ const target = _goResolveType(index, iface.file, field.fieldType || field.name);
1494
+ const inherited = target?.kind === 'iface'
1495
+ ? _goRequirements(index, target.key, visiting) : null;
1496
+ if (!inherited) {
1497
+ result.open = true;
1498
+ result.openVia.push(String(field.fieldType || field.name));
1499
+ continue;
1500
+ }
1501
+ if (inherited.open) {
1502
+ result.open = true;
1503
+ result.openVia.push(...inherited.openVia);
1504
+ }
1505
+ for (const [name, requirement] of inherited.methods) {
1506
+ if (!result.methods.has(name)) result.methods.set(name, requirement);
1507
+ else if (result.methods.get(name).def !== requirement.def) {
1508
+ _goAddMethod(result.methods, name, requirement.fingerprint, requirement.defs);
1509
+ }
1510
+ }
1511
+ }
1512
+ visiting.delete(key);
1513
+ go.requirements.set(key, result);
1514
+ return result;
1515
+ }
1516
+
1517
+ /**
1518
+ * Method set of a project named type, including methods promoted through
1519
+ * embedded fields (structs and interfaces). Pointer and value receivers are
1520
+ * unioned: every consumer here reasons about compiling code, where a
1521
+ * conversion already proved the needed receiver form. Promotions of the same
1522
+ * name from two embeddings are ambiguous selectors (not in the set).
1523
+ */
1524
+ function _goMethodSet(index, key, visiting = new Set()) {
1525
+ const go = _goState(index);
1526
+ if (go.methodSets.has(key)) return go.methodSets.get(key);
1527
+ if (go.ifaces.has(key)) {
1528
+ const requirements = _goRequirements(index, key);
1529
+ go.methodSets.set(key, requirements);
1530
+ return requirements;
1531
+ }
1532
+ const variants = go.types.get(key);
1533
+ if (!variants || visiting.has(key)) return null;
1534
+ visiting.add(key);
1535
+ const result = { methods: new Map(), open: false, openVia: [], conflicts: new Set() };
1536
+ if (variants.some(typeDef => typeDef.generics)) {
1537
+ result.open = true; result.openVia.push('type parameters');
1538
+ }
1539
+ for (const method of go.methods.get(key) || []) {
1540
+ _goAddMethod(result.methods, method.name, goMethodFingerprint(index, method), [method]);
1541
+ }
1542
+ const promoted = new Map();
1543
+ for (const field of go.fields.get(key) || []) {
1544
+ const typeDef = variants.find(variant => _goInRange(field, variant));
1545
+ if (!field.embedded || !typeDef) continue;
1546
+ const spelled = String(field.fieldType || field.name);
1547
+ const target = _goResolveType(index, typeDef.file, spelled);
1548
+ const inner = target && (target.kind === 'iface' || target.kind === 'type')
1549
+ ? _goMethodSet(index, target.key, visiting) : null;
1550
+ if (!inner) {
1551
+ result.open = true;
1552
+ result.openVia.push(spelled.replace(/^\*/, ''));
1553
+ continue;
1554
+ }
1555
+ if (inner.open) {
1556
+ result.open = true;
1557
+ result.openVia.push(...inner.openVia);
1558
+ }
1559
+ for (const [name, method] of inner.methods) {
1560
+ if (result.methods.has(name)) continue; // direct method shadows
1561
+ const previous = promoted.get(name);
1562
+ if (previous && previous.def !== method.def) result.conflicts.add(name);
1563
+ else promoted.set(name, method);
1564
+ }
1565
+ for (const name of inner.conflicts) {
1566
+ if (!result.methods.has(name)) result.conflicts.add(name);
1567
+ }
1568
+ }
1569
+ for (const [name, method] of promoted) {
1570
+ if (!result.conflicts.has(name)) result.methods.set(name, method);
1571
+ }
1572
+ visiting.delete(key);
1573
+ go.methodSets.set(key, result);
1574
+ return result;
1575
+ }
1576
+
1577
+ function _goMethodIndexes(index) {
1578
+ const go = _goState(index);
1579
+ if (go.typesByMethod) return go;
1580
+ go.typesByMethod = new Map();
1581
+ go.ifacesByMethod = new Map();
1582
+ const push = (map, name, key) => {
1583
+ if (!map.has(name)) map.set(name, []);
1584
+ map.get(name).push(key);
1585
+ };
1586
+ for (const key of go.types.keys()) {
1587
+ if (go.ambiguous.has(key)) continue;
1588
+ const set = _goMethodSet(index, key);
1589
+ for (const name of set?.methods.keys() || []) push(go.typesByMethod, name, key);
1590
+ }
1591
+ for (const key of go.ifaces.keys()) {
1592
+ if (go.ambiguous.has(key)) continue;
1593
+ const requirements = _goRequirements(index, key);
1594
+ for (const name of requirements?.methods.keys() || []) push(go.ifacesByMethod, name, key);
1595
+ }
1596
+ return go;
1597
+ }
1598
+
1599
+ /** Declared type of a project field `owner.field`, as {file, spelled}. */
1600
+ function _goFieldType(index, ownerKey, fieldName) {
1601
+ const go = _goState(index);
1602
+ const variants = go.types.get(ownerKey);
1603
+ if (!variants) return null;
1604
+ const field = (go.fields.get(ownerKey) || []).find(f =>
1605
+ f.name === fieldName && !f.embedded && variants.some(owner => _goInRange(f, owner)));
1606
+ return field?.fieldType ? { file: field.file, spelled: field.fieldType } : null;
1607
+ }
1608
+
1609
+ /**
1610
+ * Project callee definitions a call record names: a receiver-typed method
1611
+ * (direct or promoted), a package-qualified function, or a same-package
1612
+ * function. Receivers without a known type resolve nothing.
1613
+ */
1614
+ function _goCalleeDefs(index, file, slot) {
1615
+ let candidates = (index.symbols.get(slot.callee) || []).filter(d =>
1616
+ d.file && index.files.get(d.file)?.language === 'go' &&
1617
+ !NON_CALLABLE_TYPES.has(d.type));
1618
+ if (slot.receiverType) {
1619
+ const owner = _goResolveType(index, file, slot.receiverType);
1620
+ if (!owner?.key) return [];
1621
+ candidates = candidates.filter(d => d.className &&
1622
+ _goKey(path.dirname(d.file), d.className) === owner.key);
1623
+ if (candidates.length === 0) {
1624
+ const promoted = _goMethodSet(index, owner.key)?.methods.get(slot.callee);
1625
+ if (promoted?.def) candidates = [promoted.def];
1626
+ }
1627
+ return candidates;
1628
+ }
1629
+ if (slot.receiver) {
1630
+ const dir = _goQualifierDir(index, file, slot.receiver);
1631
+ if (!dir || dir.startsWith('ext:')) return [];
1632
+ return candidates.filter(d => !d.className && path.dirname(d.file) === dir);
1633
+ }
1634
+ return candidates.filter(d => !d.className && path.dirname(d.file) === path.dirname(file));
1635
+ }
1636
+
1637
+ /**
1638
+ * The declared result type (at `call.index`, default 0) of the callee a call
1639
+ * names: { file, spelled } for a project callee, { external } when the
1640
+ * callee or its receiver type lives outside the project, else null.
1641
+ */
1642
+ function _goCallResultType(index, file, call) {
1643
+ if (call.receiver && !call.receiverType) {
1644
+ const dir = _goQualifierDir(index, file, call.receiver);
1645
+ if (dir && dir.startsWith('ext:')) return { external: `${call.receiver}.${call.callee}` };
1646
+ }
1647
+ if (call.receiverType) {
1648
+ const owner = _goResolveType(index, file, call.receiverType);
1649
+ if (owner?.kind === 'external') return { external: `${owner.name}.${call.callee}` };
1650
+ }
1651
+ const results = new Set();
1652
+ let found = null;
1653
+ const position = Number.isInteger(call.index) ? call.index : 0;
1654
+ for (const def of _goCalleeDefs(index, file, call)) {
1655
+ const list = def.returnType ? splitGoSignatureList(def.returnType) : [];
1656
+ // A call used directly as one value must have exactly one result.
1657
+ if (!Number.isInteger(call.index) && list.length !== 1) return null;
1658
+ if (!list[position]) return null;
1659
+ const spelled = goDeclarationType(list[position]);
1660
+ if (!spelled) return null;
1661
+ results.add(_goCanonicalType(index, def.file, spelled));
1662
+ found = { file: def.file, spelled };
1663
+ }
1664
+ return results.size === 1 ? found : null;
1665
+ }
1666
+
1667
+ /** Parameter type at argIndex of the project callee an argument record names. */
1668
+ function _goArgumentSlot(index, file, slot) {
1669
+ const candidates = _goCalleeDefs(index, file, slot)
1670
+ .filter(d => Array.isArray(d.paramsStructured));
1671
+ const slots = new Set();
1672
+ let owner = null;
1673
+ for (const def of candidates) {
1674
+ const params = def.paramsStructured;
1675
+ let param = params[slot.argIndex];
1676
+ if (!param) {
1677
+ const last = params[params.length - 1];
1678
+ if (last && String(last.type || '').startsWith('...')) param = last;
1679
+ }
1680
+ if (!param || !param.type) return null;
1681
+ const spelled = String(param.type).replace(/^\.\.\./, '');
1682
+ slots.add(_goCanonicalType(index, def.file, spelled));
1683
+ owner = { file: def.file, spelled };
1684
+ }
1685
+ return slots.size === 1 ? owner : null;
1686
+ }
1687
+
1688
+ /**
1689
+ * The out-of-project callee an argument record names (`sort.Sort`, a method
1690
+ * of an out-of-project type), or null (fix #384).
1691
+ */
1692
+ function _goExternalCallee(index, file, slot) {
1693
+ if (slot.receiverType) {
1694
+ const owner = _goResolveType(index, file, slot.receiverType);
1695
+ return owner?.kind === 'external' ? `${owner.name}.${slot.callee}` : null;
1696
+ }
1697
+ if (slot.receiver) {
1698
+ const dir = _goQualifierDir(index, file, slot.receiver);
1699
+ return dir && dir.startsWith('ext:') ? `${slot.receiver}.${slot.callee}` : null;
1700
+ }
1701
+ return null;
1702
+ }
1703
+
1704
+ /**
1705
+ * Interface-conversion evidence resolved against the project: concrete type
1706
+ * key -> [{ target, site }], built once per index from the parser's
1707
+ * typeConversions records.
1708
+ */
1709
+ function _goConversions(index) {
1710
+ const go = _goState(index);
1711
+ if (go.conversions) return go.conversions;
1712
+ const byConcrete = new Map();
1713
+ const externalByIface = new Map();
1714
+ for (const [file, entry] of index.files) {
1715
+ if (entry.language !== 'go' || !Array.isArray(entry.typeConversions)) continue;
1716
+ for (const record of entry.typeConversions) {
1717
+ let concrete = null;
1718
+ if (record.concrete) {
1719
+ concrete = _goResolveType(index, file, record.concrete);
1720
+ } else if (record.concreteCall) {
1721
+ const result = _goCallResultType(index, file, record.concreteCall);
1722
+ concrete = result?.external ? { kind: 'external-value', name: result.external }
1723
+ : result ? _goResolveType(index, result.file, result.spelled) : null;
1724
+ } else if (record.concreteField) {
1725
+ const owner = _goResolveType(index, file, record.concreteField.owner);
1726
+ const fieldType = owner?.key
1727
+ ? _goFieldType(index, owner.key, record.concreteField.field) : null;
1728
+ concrete = fieldType ? _goResolveType(index, fieldType.file, fieldType.spelled) : null;
1729
+ }
1730
+ if (!concrete) continue;
1731
+ if (concrete.kind === 'external') {
1732
+ concrete = { kind: 'external-value', name: concrete.name };
1733
+ }
1734
+ if (concrete.kind !== 'type' && concrete.kind !== 'external-value') continue;
1735
+ let slot = null;
1736
+ // A position whose declared type lives outside the project (an
1737
+ // out-of-project function's parameter, an out-of-project
1738
+ // struct's field): its type may be an interface UCN cannot see
1739
+ // (fix #384, `sort.Sort(x.(*T))`).
1740
+ let externalPosition = null;
1741
+ if (record.slot) slot = { file, spelled: record.slot };
1742
+ else if (record.fieldSlot) {
1743
+ const owner = _goResolveType(index, file, record.fieldSlot.owner);
1744
+ slot = owner?.key ? _goFieldType(index, owner.key, record.fieldSlot.field) : null;
1745
+ if (owner?.kind === 'external' && concrete.kind === 'type') {
1746
+ externalPosition = { kind: 'external-position', position: 'field',
1747
+ name: `${owner.name}.${record.fieldSlot.field}` };
1748
+ }
1749
+ } else if (record.argSlot) {
1750
+ slot = _goArgumentSlot(index, file, record.argSlot);
1751
+ // Only a project type's value can lose a method it renames.
1752
+ const callee = slot || concrete.kind !== 'type' ? null
1753
+ : _goExternalCallee(index, file, record.argSlot);
1754
+ if (callee) {
1755
+ externalPosition = { kind: 'external-position', position: 'argument',
1756
+ name: callee, argIndex: record.argSlot.argIndex };
1757
+ }
1758
+ }
1759
+ if (!slot && !externalPosition) continue;
1760
+ const target = externalPosition || _goResolveType(index, slot.file, slot.spelled);
1761
+ if (!target || target.kind === 'other' || target.kind === 'type' ||
1762
+ (target.kind === 'builtin' && target.name !== 'error')) continue;
1763
+ if (target.pointer) continue; // pointer-to-named slots are not interfaces
1764
+ if (externalPosition && concrete.kind !== 'type') continue;
1765
+ const site = {
1766
+ file,
1767
+ relativePath: entry.relativePath || file,
1768
+ line: record.line,
1769
+ kind: record.kind,
1770
+ };
1771
+ if (concrete.kind === 'external-value') {
1772
+ // A value of an out-of-project type used as a project
1773
+ // interface: its methods cannot be renamed with the slot.
1774
+ if (target.kind !== 'iface') continue;
1775
+ if (!externalByIface.has(target.key)) externalByIface.set(target.key, []);
1776
+ externalByIface.get(target.key).push({ origin: concrete.name, site });
1777
+ continue;
1778
+ }
1779
+ if (!byConcrete.has(concrete.key)) byConcrete.set(concrete.key, []);
1780
+ byConcrete.get(concrete.key).push({ target, site });
1781
+ }
1782
+ }
1783
+ const bySite = (a, b) => (a.site.relativePath < b.site.relativePath ? -1
1784
+ : a.site.relativePath > b.site.relativePath ? 1 : a.site.line - b.site.line);
1785
+ for (const list of byConcrete.values()) list.sort(bySite);
1786
+ for (const list of externalByIface.values()) list.sort(bySite);
1787
+ go.conversions = byConcrete;
1788
+ go.externalValues = externalByIface;
1789
+ return byConcrete;
1790
+ }
1791
+
1792
+ function _goTypeLabel(index, key) {
1793
+ const go = _goState(index);
1794
+ const defs = go.types.get(key) || go.ifaces.get(key);
1795
+ return defs ? defs[0].name : key.split('\0').pop();
1796
+ }
1797
+
1798
+ /**
1799
+ * Satisfaction of an interface by a named type from their method sets:
1800
+ * 'yes' (every requirement present with the same canonical fingerprint and
1801
+ * the requirement set is closed), 'no' (a requirement provably missing or
1802
+ * mismatched), 'maybe' (open method set or open requirement set).
1803
+ */
1804
+ function _goSatisfies(index, typeKey, ifaceKey) {
1805
+ const methods = _goMethodSet(index, typeKey);
1806
+ const requirements = _goRequirements(index, ifaceKey);
1807
+ if (!methods || !requirements) return 'maybe';
1808
+ let unknown = requirements.open;
1809
+ for (const [name, requirement] of requirements.methods) {
1810
+ const method = methods.methods.get(name);
1811
+ if (!method) {
1812
+ if (methods.open || methods.conflicts.has(name)) { unknown = true; continue; }
1813
+ return 'no';
1814
+ }
1815
+ if (method.defs.some(d => requirement.defs.includes(d))) continue;
1816
+ if (!method.fingerprint || !requirement.fingerprint) { unknown = true; continue; }
1817
+ if (method.fingerprint !== requirement.fingerprint) return 'no';
1818
+ }
1819
+ return unknown ? 'maybe' : 'yes';
1820
+ }
1821
+
1822
+ function _defIdentity(def) {
1823
+ return `${path.resolve(def.file)}\0${def.startLine}`;
1824
+ }
1825
+
1826
+ /**
1827
+ * Go rename closure for a method or interface-method definition: the
1828
+ * bipartite fixed point between interfaces requiring the name and named
1829
+ * types whose method sets carry it. A type joins a joined interface when it
1830
+ * structurally satisfies it (complete method sets) or when the project
1831
+ * converts it to that interface (compiler evidence that also covers
1832
+ * spelling-level gaps); an interface joins when a joined type satisfies it;
1833
+ * interfaces sharing a joined requirement by embedding and types sharing a
1834
+ * joined method by promotion join too. Implementer-side and interface-side
1835
+ * pins reach the same fixed point.
1836
+ *
1837
+ * Returns null when the name has no interface participation, else
1838
+ * { memberDefs, memberIdentity, interfaceNames, reviews }
1839
+ * where reviews name every place the closure cannot be completed by
1840
+ * editing project declarations: types converted to external or open
1841
+ * interfaces, types that may satisfy a joined interface but have open
1842
+ * method sets, and types that get the method from an external embedding.
1843
+ */
1844
+ function goContractClosure(index, def) {
1845
+ if (!def?.className || index.files.get(def.file)?.language !== 'go') return null;
1846
+ const go = _goMethodIndexes(index);
1847
+ const name = def.name;
1848
+ const dir = path.dirname(def.file);
1849
+ const ownerKey = _goKey(dir, def.className);
1850
+ const typeNodes = go.typesByMethod.get(name) || [];
1851
+ const ifaceNodes = go.ifacesByMethod.get(name) || [];
1852
+ const conversions = _goConversions(index);
1853
+ const convertedTo = (typeKey, ifaceKey) => (conversions.get(typeKey) || [])
1854
+ .some(item => item.target.kind === 'iface' && item.target.key === ifaceKey);
1855
+
1856
+ const joinedTypes = new Set();
1857
+ const joinedIfaces = new Set();
1858
+ if (def.type === 'method' && go.ifaces.has(ownerKey)) {
1859
+ joinedIfaces.add(ownerKey);
1860
+ } else {
1861
+ for (const key of typeNodes) {
1862
+ if (_goMethodSet(index, key)?.methods.get(name)?.defs.includes(def)) joinedTypes.add(key);
1863
+ }
1864
+ if (joinedTypes.size === 0 && go.types.has(ownerKey)) joinedTypes.add(ownerKey);
1865
+ }
1866
+ if (joinedTypes.size === 0 && joinedIfaces.size === 0) return null;
1867
+
1868
+ const joinedDefs = new Set();
1869
+ const noteDefs = () => {
1870
+ for (const key of joinedTypes) {
1871
+ const method = _goMethodSet(index, key)?.methods.get(name);
1872
+ for (const d of method?.defs || []) joinedDefs.add(d);
1873
+ }
1874
+ for (const key of joinedIfaces) {
1875
+ const requirement = _goRequirements(index, key)?.methods.get(name);
1876
+ for (const d of requirement?.defs || []) joinedDefs.add(d);
1877
+ }
1878
+ };
1879
+ noteDefs();
1880
+ let changed = true;
1881
+ let rounds = 0;
1882
+ while (changed && rounds++ < 64) {
1883
+ changed = false;
1884
+ for (const ifaceKey of ifaceNodes) {
1885
+ for (const typeKey of typeNodes) {
1886
+ if (joinedIfaces.has(ifaceKey) === joinedTypes.has(typeKey)) continue;
1887
+ const proven = _goSatisfies(index, typeKey, ifaceKey) === 'yes' ||
1888
+ convertedTo(typeKey, ifaceKey);
1889
+ if (!proven) continue;
1890
+ if (joinedIfaces.has(ifaceKey)) joinedTypes.add(typeKey);
1891
+ else joinedIfaces.add(ifaceKey);
1892
+ changed = true;
1893
+ }
1894
+ }
1895
+ noteDefs();
1896
+ for (const ifaceKey of ifaceNodes) {
1897
+ if (joinedIfaces.has(ifaceKey)) continue;
1898
+ const requirement = _goRequirements(index, ifaceKey)?.methods.get(name);
1899
+ if (requirement?.defs.some(d => joinedDefs.has(d))) {
1900
+ joinedIfaces.add(ifaceKey); changed = true;
1901
+ }
1902
+ }
1903
+ for (const typeKey of typeNodes) {
1904
+ if (joinedTypes.has(typeKey)) continue;
1905
+ const method = _goMethodSet(index, typeKey)?.methods.get(name);
1906
+ if (method?.defs.some(d => joinedDefs.has(d))) {
1907
+ joinedTypes.add(typeKey); changed = true;
1908
+ }
1909
+ }
1910
+ noteDefs();
1911
+ }
1912
+
1913
+ const reviews = [];
1914
+ const reviewKeys = new Set();
1915
+ const addReview = (site, reason, message) => {
1916
+ const key = `${site.relativePath}:${site.line}:${reason}`;
1917
+ if (reviewKeys.has(key)) return;
1918
+ reviewKeys.add(key);
1919
+ reviews.push({ ...site, reason, message });
1920
+ };
1921
+ const ifaceLabel = key => _goTypeLabel(index, key);
1922
+ const joinedIfaceNames = [...joinedIfaces].map(ifaceLabel);
1923
+ // (1) Types that may satisfy a joined interface but whose method set (or
1924
+ // the interface's requirement set) is open, with no conversion evidence.
1925
+ for (const typeKey of typeNodes) {
1926
+ if (joinedTypes.has(typeKey)) continue;
1927
+ for (const ifaceKey of joinedIfaces) {
1928
+ if (_goSatisfies(index, typeKey, ifaceKey) !== 'maybe') continue;
1929
+ const method = _goMethodSet(index, typeKey)?.methods.get(name);
1930
+ if (!method?.def || method.defs.some(d => joinedDefs.has(d))) continue;
1931
+ const openVia = [...new Set([
1932
+ ...(_goMethodSet(index, typeKey)?.openVia || []),
1933
+ ...(_goRequirements(index, ifaceKey)?.openVia || []),
1934
+ ])];
1935
+ addReview({
1936
+ file: method.def.file,
1937
+ relativePath: method.def.relativePath || _rel(index, method.def.file),
1938
+ line: method.def.nameLine || method.def.startLine,
1939
+ }, 'go-possible-satisfier',
1940
+ `${_goTypeLabel(index, typeKey)}.${name} may satisfy ${ifaceLabel(ifaceKey)}` +
1941
+ (openVia.length > 0 ? ` (method set open through ${openVia.join(', ')})` : '') +
1942
+ '; rename it too if the type is used as that interface');
1943
+ break;
1944
+ }
1945
+ }
1946
+ // (1b) Symmetric: interfaces a joined type may satisfy, but whose
1947
+ // satisfaction cannot be proven from complete method sets and has no
1948
+ // conversion evidence.
1949
+ for (const ifaceKey of ifaceNodes) {
1950
+ if (joinedIfaces.has(ifaceKey)) continue;
1951
+ const requirement = _goRequirements(index, ifaceKey)?.methods.get(name);
1952
+ if (!requirement?.def || requirement.defs.some(d => joinedDefs.has(d))) continue;
1953
+ const typeKey = [...joinedTypes].find(key => _goSatisfies(index, key, ifaceKey) === 'maybe');
1954
+ if (!typeKey) continue;
1955
+ const openVia = [...new Set([
1956
+ ...(_goMethodSet(index, typeKey)?.openVia || []),
1957
+ ...(_goRequirements(index, ifaceKey)?.openVia || []),
1958
+ ])];
1959
+ addReview({
1960
+ file: requirement.def.file,
1961
+ relativePath: requirement.def.relativePath || _rel(index, requirement.def.file),
1962
+ line: requirement.def.nameLine || requirement.def.startLine,
1963
+ }, 'go-possible-interface',
1964
+ `${_goTypeLabel(index, typeKey)} may satisfy ${ifaceLabel(ifaceKey)}` +
1965
+ (openVia.length > 0 ? ` (method set open through ${openVia.join(', ')})` : '') +
1966
+ `; rename ${ifaceLabel(ifaceKey)}.${name} too if the type is used as that interface`);
1967
+ }
1968
+ // (2) Joined types used as interfaces outside the closure.
1969
+ const exported = /^[A-Z]/.test(name);
1970
+ for (const typeKey of joinedTypes) {
1971
+ for (const { target, site } of conversions.get(typeKey) || []) {
1972
+ const typeLabel = _goTypeLabel(index, typeKey);
1973
+ if (target.kind === 'iface') {
1974
+ if (joinedIfaces.has(target.key)) continue;
1975
+ const requirements = _goRequirements(index, target.key);
1976
+ if (!requirements) continue;
1977
+ if (requirements.methods.has(name)) {
1978
+ addReview(site, 'go-interface-outside-closure',
1979
+ `${typeLabel} is used as ${ifaceLabel(target.key)}, which requires ` +
1980
+ `${name} but could not be joined to the rename; rename it together`);
1981
+ } else if (requirements.open) {
1982
+ addReview(site, 'go-open-interface',
1983
+ `${typeLabel} is used as ${ifaceLabel(target.key)}, whose method set embeds ` +
1984
+ `${[...new Set(requirements.openVia)].join(', ')} (outside the project); ` +
1985
+ `if that requires ${name}, renaming breaks ${typeLabel}'s satisfaction`);
1986
+ }
1987
+ } else if (target.kind === 'external' ||
1988
+ (target.kind === 'builtin' && target.name === 'error' && name === 'Error')) {
1989
+ // An unexported method never satisfies an interface
1990
+ // declared in another package.
1991
+ if (!exported) continue;
1992
+ addReview(site, 'go-external-interface',
1993
+ `${typeLabel} is used as external interface ${target.name}; if ${target.name} ` +
1994
+ `requires ${name}, renaming breaks ${typeLabel}'s satisfaction`);
1995
+ } else if (target.kind === 'external-position' && exported) {
1996
+ const position = target.position === 'argument'
1997
+ ? `passed to ${target.name} (argument ${target.argIndex + 1})`
1998
+ : `assigned to field ${target.name}`;
1999
+ addReview(site, 'go-external-position',
2000
+ `${typeLabel} is ${position}, whose type is declared outside the project; ` +
2001
+ `if it is an interface requiring ${name}, renaming breaks ${typeLabel}'s satisfaction`);
2002
+ }
2003
+ }
2004
+ }
2005
+ // (3) Types converted to a joined interface that get the method from an
2006
+ // external embedding: their slot cannot be renamed in project code.
2007
+ for (const [typeKey, list] of conversions) {
2008
+ if (joinedTypes.has(typeKey)) continue;
2009
+ const methods = _goMethodSet(index, typeKey);
2010
+ if (!methods || methods.methods.has(name)) continue;
2011
+ for (const { target, site } of list) {
2012
+ if (target.kind !== 'iface' || !joinedIfaces.has(target.key)) continue;
2013
+ addReview(site, 'go-external-satisfier',
2014
+ `${_goTypeLabel(index, typeKey)} is used as ${ifaceLabel(target.key)} but gets ` +
2015
+ `${name} from ${methods.openVia.join(', ') || 'an embedding'} outside the project`);
2016
+ }
2017
+ }
2018
+ // (4) Values of out-of-project types used as a joined interface
2019
+ // (`f, err := os.Create(p); return f, err` as afero.File): their method
2020
+ // is fixed outside the project, so renaming the slot breaks them.
2021
+ for (const ifaceKey of joinedIfaces) {
2022
+ const requirement = _goRequirements(index, ifaceKey)?.methods.get(name);
2023
+ if (!requirement) continue;
2024
+ for (const { origin, site } of go.externalValues.get(ifaceKey) || []) {
2025
+ addReview(site, 'go-external-satisfier',
2026
+ `a value from ${origin} (outside the project) is used as ${ifaceLabel(ifaceKey)}; ` +
2027
+ `its ${name} cannot be renamed with the interface`);
2028
+ }
2029
+ }
2030
+ reviews.sort((a, b) => (a.relativePath < b.relativePath ? -1
2031
+ : a.relativePath > b.relativePath ? 1 : a.line - b.line));
2032
+
2033
+ if (joinedIfaces.size === 0 && reviews.length === 0) return null;
2034
+ const memberDefs = [...joinedDefs].sort((a, b) =>
2035
+ (a.relativePath || a.file) < (b.relativePath || b.file) ? -1
2036
+ : (a.relativePath || a.file) > (b.relativePath || b.file) ? 1
2037
+ : a.startLine - b.startLine);
2038
+ return {
2039
+ memberDefs,
2040
+ memberIdentity: new Set(memberDefs.map(_defIdentity)),
2041
+ interfaceNames: new Set(joinedIfaceNames),
2042
+ reviews,
2043
+ };
2044
+ }
2045
+
2046
+ // ---------------------------------------------------------------------------
2047
+ // Rust macro_rules! templates
2048
+ // ---------------------------------------------------------------------------
2049
+
2050
+ function _children(node) {
2051
+ const out = [];
2052
+ for (let i = 0; i < node.childCount; i++) out.push(node.child(i));
2053
+ return out;
2054
+ }
2055
+
2056
+ /**
2057
+ * The item header a token tree is the body of, read backwards from its
2058
+ * siblings: `fn NAME (...) -> T { <tree> }` -> { kind: 'fn', name };
2059
+ * `impl [<..>] [Trait] for Type { <tree> }` -> { kind: 'impl', trait } (trait
2060
+ * null for inherent impls). null when the tree is not an item body.
2061
+ */
2062
+ function _tokenTreeHeader(tree) {
2063
+ const parent = tree.parent;
2064
+ if (!parent) return null;
2065
+ const siblings = _children(parent);
2066
+ const at = siblings.findIndex(sibling => sibling.id === tree.id);
2067
+ if (at <= 0) return null;
2068
+ const tokens = [];
2069
+ for (let i = at - 1; i >= 0; i--) {
2070
+ const token = siblings[i];
2071
+ if (token.type === ';' || token.type === '=>' ||
2072
+ (token.type === 'token_tree' && token.text.startsWith('{'))) break;
2073
+ tokens.unshift(token);
2074
+ if (token.type === 'fn' || token.type === 'impl') break;
2075
+ }
2076
+ if (tokens.length === 0) return null;
2077
+ if (tokens[0].type === 'fn') {
2078
+ const nameToken = tokens[1];
2079
+ return nameToken?.type === 'identifier' ? { kind: 'fn', name: nameToken.text } : null;
2080
+ }
2081
+ if (tokens[0].type === 'impl') {
2082
+ const forAt = tokens.findIndex(token => token.type === 'for');
2083
+ if (forAt < 0) return { kind: 'impl', trait: null };
2084
+ const path = tokens.slice(1, forAt)
2085
+ .filter(token => token.type === 'identifier').map(token => token.text);
2086
+ return { kind: 'impl', trait: path.length > 0 ? path[path.length - 1] : null };
2087
+ }
2088
+ return null;
2089
+ }
2090
+
2091
+ /**
2092
+ * Identifier tokens spelling `name` inside macro_rules! transcribers of the
2093
+ * given Rust files, classified by their token shape:
2094
+ * definition `fn NAME` (with the impl header or invocation contexts that
2095
+ * give it a trait/inherent/module owner)
2096
+ * method-call `.NAME(` path-call `X::NAME(` call `NAME(` other
2097
+ * Each site carries the enclosing template fn (`fn get_i8 { (**self).get_i8() }`
2098
+ * forwards to the same slot) and the macro name. Token level only: what a
2099
+ * template token binds to is decided by the caller from the pin's identity.
2100
+ */
2101
+ function rustMacroTemplateSites(index, name, files) {
2102
+ const { getParser, safeParse } = require('../languages');
2103
+ const parser = getParser('rust');
2104
+ if (!parser) return [];
2105
+ const sites = [];
2106
+ for (const file of files) {
2107
+ const entry = index.files.get(file);
2108
+ if (!entry || entry.language !== 'rust') continue;
2109
+ let content;
2110
+ try { content = index._readFile(file); } catch { continue; }
2111
+ // Only files that define a macro can hold template tokens; the
2112
+ // AST below decides everything, this only skips needless parses.
2113
+ if (!content.includes('macro_rules!')) continue;
2114
+ const tree = index._getParsedTree?.(file, content, 'rust') || safeParse(parser, content);
2115
+ if (!tree) continue;
2116
+ const macros = [];
2117
+ const invocations = [];
2118
+ const stack = [tree.rootNode];
2119
+ while (stack.length > 0) {
2120
+ const node = stack.pop();
2121
+ if (node.type === 'macro_definition') { macros.push(node); continue; }
2122
+ if (node.type === 'macro_invocation') invocations.push(node);
2123
+ for (let i = node.namedChildCount - 1; i >= 0; i--) stack.push(node.namedChild(i));
2124
+ }
2125
+ if (macros.length === 0) continue;
2126
+ const invocationContexts = (macroName) => {
2127
+ const contexts = [];
2128
+ for (const invocation of invocations) {
2129
+ const macroNode = invocation.childForFieldName('macro');
2130
+ if (!macroNode || macroNode.text.split('::').pop() !== macroName) continue;
2131
+ let owner = { kind: 'module' };
2132
+ for (let p = invocation.parent; p; p = p.parent) {
2133
+ if (p.type === 'impl_item') {
2134
+ const traitNode = p.childForFieldName('trait');
2135
+ owner = {
2136
+ kind: 'impl',
2137
+ trait: traitNode ? traitNode.text.replace(/<.*$/s, '').split('::').pop() : null,
2138
+ line: p.startPosition.row + 1,
2139
+ };
2140
+ break;
2141
+ }
2142
+ if (p.type === 'function_item') { owner = { kind: 'fn-body' }; break; }
2143
+ }
2144
+ contexts.push({ ...owner, invocationLine: invocation.startPosition.row + 1 });
2145
+ }
2146
+ return contexts;
2147
+ };
2148
+ for (const macro of macros) {
2149
+ const macroName = macro.childForFieldName('name')?.text ||
2150
+ macro.namedChildren.find(child => child.type === 'identifier')?.text || null;
2151
+ let contexts = null;
2152
+ const walk = (node, inTranscriber) => {
2153
+ for (const child of _children(node)) {
2154
+ if (child.type === 'token_tree_pattern') continue;
2155
+ if (child.type === 'macro_rule') { walk(child, false); continue; }
2156
+ if (child.type === 'token_tree') {
2157
+ walk(child, inTranscriber || node.type === 'macro_rule');
2158
+ continue;
2159
+ }
2160
+ if (!inTranscriber || child.type !== 'identifier' || child.text !== name) continue;
2161
+ const siblings = _children(node);
2162
+ const at = siblings.findIndex(sibling => sibling.id === child.id);
2163
+ const prev = siblings[at - 1];
2164
+ const next = siblings[at + 1];
2165
+ const callArgs = next?.type === 'token_tree' && next.text.startsWith('(');
2166
+ let shape = 'other';
2167
+ if (prev?.type === '$') continue; // metavariable, not the name
2168
+ if (prev?.type === 'fn') shape = 'definition';
2169
+ else if (prev?.type === '.' && callArgs) shape = 'method-call';
2170
+ else if (prev?.type === '::' && callArgs) shape = 'path-call';
2171
+ else if (callArgs) shape = 'call';
2172
+ // Enclosing template item headers, innermost first.
2173
+ let enclosingFn = null;
2174
+ let innerImpl = null;
2175
+ for (let t = node; t && t.type === 'token_tree'; t = t.parent) {
2176
+ const header = _tokenTreeHeader(t);
2177
+ if (!header) continue;
2178
+ if (header.kind === 'fn' && !enclosingFn && shape !== 'definition') {
2179
+ enclosingFn = header.name;
2180
+ }
2181
+ if (header.kind === 'impl' && !innerImpl) innerImpl = header;
2182
+ }
2183
+ if (!contexts) contexts = macroName ? invocationContexts(macroName) : [];
2184
+ sites.push({
2185
+ file,
2186
+ relativePath: entry.relativePath || file,
2187
+ line: child.startPosition.row + 1,
2188
+ column: child.startPosition.column,
2189
+ shape,
2190
+ macroName,
2191
+ enclosingFn,
2192
+ owners: innerImpl
2193
+ ? [{ kind: 'impl', trait: innerImpl.trait }]
2194
+ : contexts,
2195
+ ...(prev?.type === '::' && {
2196
+ pathQualifier: siblings[at - 2]?.text || null,
2197
+ }),
2198
+ });
2199
+ }
2200
+ };
2201
+ walk(macro, false);
2202
+ }
2203
+ }
2204
+ sites.sort((a, b) => (a.relativePath < b.relativePath ? -1
2205
+ : a.relativePath > b.relativePath ? 1 : a.line - b.line || a.column - b.column));
2206
+ return sites;
2207
+ }
2208
+
2209
+ /**
2210
+ * Project Go types and interfaces whose selector `T.<memberName>` resolves to
2211
+ * a project declaration (fix #383): methods (own and promoted through
2212
+ * embedding, ambiguous promotions excluded), interface requirements, and
2213
+ * directly declared fields. Returns [{ dir, typeName, defs }] sorted by key;
2214
+ * `defs` are the declarations the selector reaches (build variants joined).
2215
+ */
2216
+ function goMemberProviders(index, memberName) {
2217
+ const go = _goMethodIndexes(index);
2218
+ const out = [];
2219
+ const typeNameOf = key => key.slice(key.indexOf('\0') + 1);
2220
+ const dirOf = key => key.slice(0, key.indexOf('\0'));
2221
+ for (const key of go.typesByMethod.get(memberName) || []) {
2222
+ const entry = _goMethodSet(index, key)?.methods.get(memberName);
2223
+ if (entry) out.push({ key, dir: dirOf(key), typeName: typeNameOf(key), defs: entry.defs });
2224
+ }
2225
+ for (const key of go.ifacesByMethod.get(memberName) || []) {
2226
+ const entry = _goRequirements(index, key)?.methods.get(memberName);
2227
+ if (entry) out.push({ key, dir: dirOf(key), typeName: typeNameOf(key), defs: entry.defs });
2228
+ }
2229
+ for (const [key, fields] of go.fields) {
2230
+ const variants = go.types.get(key) || go.ifaces.get(key) || [];
2231
+ const own = fields.filter(field => field.name === memberName && !field.embedded &&
2232
+ variants.some(owner => _goInRange(field, owner)));
2233
+ if (own.length > 0 && !out.some(item => item.key === key)) {
2234
+ out.push({ key, dir: dirOf(key), typeName: typeNameOf(key), defs: own });
2235
+ }
2236
+ }
2237
+ return out.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
2238
+ }
2239
+
2240
+ module.exports = {
2241
+ protocolMemberOf,
2242
+ protocolMemberCertainty,
2243
+ protocolTypeOf,
2244
+ ownerDerivesFromExternal,
2245
+ rustMacroTemplateSites,
2246
+ resetContractIndex,
2247
+ ownerClassDefs,
2248
+ memberInClassBody,
2249
+ implementedBy,
2250
+ rustProjectTraits,
2251
+ rustTraitResolution,
2252
+ externalContractOf,
2253
+ hierarchySlotClosure,
2254
+ anonymousSlotMembers,
2255
+ typedLiteralContract,
2256
+ typedLiteralMembers,
2257
+ goContractClosure,
2258
+ goMethodFingerprint,
2259
+ splitGoSignatureList,
2260
+ goDeclarationType,
2261
+ goMemberProviders,
2262
+ };