ucn 5.4.2 → 5.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/.claude/skills/ucn/SKILL.md +58 -236
  2. package/.claude/skills/ucn/references/commands.md +144 -68
  3. package/.claude/skills/ucn/references/resolution-rules.md +171 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +27 -33
  5. package/README.md +27 -16
  6. package/cli/index.js +45 -14
  7. package/core/accessors.js +118 -3
  8. package/core/account.js +62 -11
  9. package/core/analysis.js +517 -266
  10. package/core/ast-analysis.js +76 -117
  11. package/core/async-producers.js +1729 -0
  12. package/core/bridge.js +1877 -268
  13. package/core/brief.js +6 -0
  14. package/core/build-worker.js +78 -14
  15. package/core/cache.js +529 -31
  16. package/core/callers.js +12228 -921
  17. package/core/check.js +30 -38
  18. package/core/class-identity.js +1028 -0
  19. package/core/command-contracts.js +6 -6
  20. package/core/configuration-items.js +86 -0
  21. package/core/contract-membership.js +2262 -0
  22. package/core/cpp-scope.js +532 -0
  23. package/core/deadcode.js +171 -49
  24. package/core/decorator-binding.js +260 -0
  25. package/core/discovery.js +281 -164
  26. package/core/errors.js +52 -0
  27. package/core/execute.js +21 -9
  28. package/core/external-macros.js +326 -0
  29. package/core/graph-build.js +277 -8
  30. package/core/graph.js +35 -2
  31. package/core/ignore-rules.js +472 -0
  32. package/core/imports.js +504 -42
  33. package/core/index-ir.js +94 -11
  34. package/core/inprocess-clients.js +636 -0
  35. package/core/ir.js +87 -8
  36. package/core/macro-expansion.js +1416 -0
  37. package/core/output/analysis.js +18 -2
  38. package/core/output/brief.js +3 -0
  39. package/core/output/doctor.js +4 -0
  40. package/core/output/endpoints.js +15 -6
  41. package/core/output/find.js +7 -2
  42. package/core/output/refactoring.js +121 -14
  43. package/core/output/reporting.js +31 -3
  44. package/core/output/shared.js +14 -0
  45. package/core/output-budget.js +305 -122
  46. package/core/parallel-build.js +124 -47
  47. package/core/parser.js +3 -2
  48. package/core/project.js +472 -66
  49. package/core/provenance-facts.js +156 -12
  50. package/core/provenance-overload.js +30 -3
  51. package/core/provenance.js +104 -30
  52. package/core/pytest-fixtures.js +434 -0
  53. package/core/python-fixture-flow.js +17 -3
  54. package/core/python-modules.js +173 -0
  55. package/core/reflection.js +877 -0
  56. package/core/registry.js +1 -1
  57. package/core/reporting.js +234 -52
  58. package/core/request-helpers.js +561 -0
  59. package/core/route-graph.js +1922 -0
  60. package/core/route-lists.js +2414 -0
  61. package/core/rust-macro-expansion.js +3026 -0
  62. package/core/rust-modules.js +203 -0
  63. package/core/search.js +121 -29
  64. package/core/shared.js +19 -1
  65. package/core/test-name-bindings.js +256 -0
  66. package/core/type-denotation.js +1185 -0
  67. package/core/type-references.js +3229 -0
  68. package/core/verify.js +3360 -672
  69. package/languages/adapter.js +31 -5
  70. package/languages/c-family.js +4061 -212
  71. package/languages/c-macro-invocations.js +380 -0
  72. package/languages/c-preprocessor.js +254 -0
  73. package/languages/csharp.js +1406 -49
  74. package/languages/go.js +1243 -70
  75. package/languages/index.js +728 -6
  76. package/languages/java.js +473 -61
  77. package/languages/javascript.js +1270 -197
  78. package/languages/lexical-scope.js +1280 -0
  79. package/languages/python.js +1557 -72
  80. package/languages/rust-macro-rules.js +1020 -0
  81. package/languages/rust-value-flow.js +416 -0
  82. package/languages/rust.js +2265 -112
  83. package/languages/utils.js +298 -4
  84. package/mcp/server.js +22 -8
  85. package/package.json +1 -1
@@ -0,0 +1,1185 @@
1
+ /**
2
+ * core/type-denotation.js - What a type NAME written at a call site denotes
3
+ * (fix #371).
4
+ *
5
+ * A call site names a type in writing: the class of a constructor call
6
+ * (`new File(..)`, `Path(..)`), the qualifier of a type-qualified call
7
+ * (`File::create(..)`, `std::fs::File::create(..)`, `File.createTempFile(..)`)
8
+ * or the annotated / constructed type of its receiver (`let h: File`,
9
+ * `File f = new File(..)`). The name denotes whatever the FILE's own
10
+ * bindings resolve it to, never whichever project type happens to share
11
+ * the spelling. This module answers one question conservatively: does the
12
+ * written name provably denote an EXTERNAL type?
13
+ *
14
+ * - a qualifier rooted at the language's standard library
15
+ * (`standardPathRoots`: Rust std/core/alloc, C++ std) or at an external
16
+ * package binding (`use tokio_util::..`, Go `bytes.Buffer`, Java
17
+ * `java.io.File`),
18
+ * - an unqualified name bound by an import of an external module
19
+ * (`use std::fs::File`, `import java.io.File`, `from pathlib import
20
+ * Path`, `import { Server } from 'http'`, `using F = System.IO.File`),
21
+ * or reached only through globs of external modules (Rust
22
+ * `use std::sync::atomic::*`).
23
+ *
24
+ * A local definition of the name, a binding that resolves (or may resolve)
25
+ * into the project, or any doubt returns null: the engine's existing
26
+ * resolution decides, and nothing is excluded without positive evidence.
27
+ *
28
+ * `externalTypeGate` turns the answer into a caller/callee verdict. The
29
+ * targets whose owner type carries the written name (the "collision set")
30
+ * cannot be the callee when that owner is a project type: excluded
31
+ * `external-receiver`. When the owner is itself the external type (a
32
+ * project `impl Trait for std::fs::File`, an impl whose file `use`s the
33
+ * external type), the call may reach it: possible dispatch (inherent
34
+ * methods of the external type win over trait methods, so it is never
35
+ * confirmed). An unrelated owner is left to the engine, with a receiver
36
+ * typed by the external name stripped so that name can never pin a
37
+ * project owner.
38
+ *
39
+ * Memoized on the index (`_typeDenotation`), reset at every build/load.
40
+ */
41
+
42
+ 'use strict';
43
+
44
+ const fs = require('fs');
45
+ const path = require('path');
46
+ const { langTraits } = require('../languages');
47
+ const { findCargoRoot, findGoModule } = require('./imports');
48
+
49
+ const TYPE_KINDS = new Set(['class', 'struct', 'interface', 'trait', 'enum', 'record', 'union']);
50
+
51
+ function memoOf(index) {
52
+ return index._typeDenotation || (index._typeDenotation = new Map());
53
+ }
54
+
55
+ function memoized(index, key, compute) {
56
+ const memo = memoOf(index);
57
+ if (memo.has(key)) return memo.get(key);
58
+ const value = compute();
59
+ memo.set(key, value);
60
+ return value;
61
+ }
62
+
63
+ // ── Package metadata (dependency names) ───────────────────────────────────
64
+
65
+ /**
66
+ * Dependencies a Cargo manifest declares, as local crate name -> 'path' |
67
+ * 'workspace' | 'registry' (all dependency tables, target-specific and
68
+ * workspace tables included; the table key is the local name, `-`
69
+ * normalized to `_`). A `path` dependency is project source.
70
+ */
71
+ function cargoDependencies(manifestDir) {
72
+ const deps = new Map();
73
+ let text;
74
+ try { text = fs.readFileSync(path.join(manifestDir, 'Cargo.toml'), 'utf-8'); } catch { return deps; }
75
+ let section = null; // 'deps' for a dependency table, a crate name for [dependencies.<name>]
76
+ const kindOf = body => /\bpath\s*=/.test(body) ? 'path'
77
+ : /\bworkspace\s*=\s*true/.test(body) ? 'workspace' : 'registry';
78
+ for (const raw of text.split('\n')) {
79
+ const line = raw.replace(/#.*$/, '').trim();
80
+ if (!line) continue;
81
+ const header = line.match(/^\[\s*([^\]]+?)\s*\]$/);
82
+ if (header) {
83
+ const table = header[1].match(/(?:^|\.)((?:dev-|build-)?dependencies)(?:\.([A-Za-z0-9_-]+))?$/);
84
+ if (table && table[2]) {
85
+ section = table[2].replace(/-/g, '_');
86
+ if (!deps.has(section)) deps.set(section, 'registry');
87
+ } else {
88
+ section = table ? 'deps' : null;
89
+ }
90
+ continue;
91
+ }
92
+ if (!section) continue;
93
+ if (section !== 'deps') {
94
+ if (/^path\s*=/.test(line)) deps.set(section, 'path');
95
+ else if (/^workspace\s*=\s*true/.test(line) && deps.get(section) !== 'path') deps.set(section, 'workspace');
96
+ continue;
97
+ }
98
+ const entry = line.match(/^([A-Za-z0-9_-]+)\s*(?:\.[A-Za-z0-9_.-]+)?\s*=(.*)$/);
99
+ if (entry) {
100
+ const name = entry[1].replace(/-/g, '_');
101
+ const kind = kindOf(entry[2]);
102
+ if (deps.get(name) !== 'path') deps.set(name, kind);
103
+ }
104
+ }
105
+ return deps;
106
+ }
107
+
108
+ function cargoPackageName(dir) {
109
+ try {
110
+ const text = fs.readFileSync(path.join(dir, 'Cargo.toml'), 'utf-8');
111
+ const section = text.split(/^\s*\[/m).find(part => part.startsWith('package]'));
112
+ const match = section && section.match(/^\s*name\s*=\s*"([^"]+)"/m);
113
+ return match ? match[1].replace(/-/g, '_') : null;
114
+ } catch { return null; }
115
+ }
116
+
117
+ /**
118
+ * Crates the project itself builds, from the root manifest alone (no tree
119
+ * walk): the root package, `[workspace] members` (simple `dir/*` globs
120
+ * expanded one level), and `[patch.*]` path overrides.
121
+ */
122
+ function cargoWorkspaceCrates(root) {
123
+ const names = new Set();
124
+ let text;
125
+ try { text = fs.readFileSync(path.join(root, 'Cargo.toml'), 'utf-8'); } catch { return names; }
126
+ const own = cargoPackageName(root);
127
+ if (own) names.add(own);
128
+ const members = text.match(/^\s*members\s*=\s*\[([\s\S]*?)\]/m);
129
+ for (const quoted of (members ? members[1].replace(/#.*$/gm, '').match(/"([^"]+)"/g) || [] : [])) {
130
+ const member = quoted.slice(1, -1);
131
+ const dirs = [];
132
+ if (member.endsWith('/*')) {
133
+ const base = path.join(root, member.slice(0, -2));
134
+ try {
135
+ for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
136
+ if (entry.isDirectory()) dirs.push(path.join(base, entry.name));
137
+ }
138
+ } catch { /* missing glob base */ }
139
+ } else if (!/[*?[]/.test(member)) {
140
+ dirs.push(path.join(root, member));
141
+ }
142
+ for (const dir of dirs) {
143
+ const name = cargoPackageName(dir);
144
+ if (name) names.add(name);
145
+ }
146
+ }
147
+ let inPatch = false;
148
+ for (const raw of text.split('\n')) {
149
+ const line = raw.replace(/#.*$/, '').trim();
150
+ const header = line.match(/^\[\s*([^\]]+?)\s*\]$/);
151
+ if (header) { inPatch = header[1].startsWith('patch.'); continue; }
152
+ const entry = inPatch && line.match(/^([A-Za-z0-9_-]+)\s*=.*\bpath\s*=/);
153
+ if (entry) names.add(entry[1].replace(/-/g, '_'));
154
+ }
155
+ return names;
156
+ }
157
+
158
+ /** External crate names visible to a Rust file: its manifest's registry
159
+ * dependencies (workspace-inherited ones resolved against the project
160
+ * root's workspace table); `path` dependencies are project crates. */
161
+ function rustExternalCrates(index, filePath) {
162
+ const cargo = findCargoRoot(path.dirname(filePath));
163
+ const manifestDir = cargo?.root || index.root;
164
+ return memoized(index, `\x01crates\x00${manifestDir}`, () => {
165
+ const root = memoized(index, '\x01crates-root', () => cargoDependencies(index.root));
166
+ const names = new Set();
167
+ for (const deps of [cargoDependencies(manifestDir), root]) {
168
+ for (const [name, kind] of deps) {
169
+ const resolved = kind === 'workspace' ? root.get(name) || 'registry' : kind;
170
+ if (resolved === 'registry') names.add(name);
171
+ }
172
+ }
173
+ for (const [name, kind] of cargoDependencies(manifestDir)) {
174
+ if (kind === 'path' || (kind === 'workspace' && root.get(name) === 'path')) names.delete(name);
175
+ }
176
+ for (const name of memoized(index, '\x01crates-workspace', () => cargoWorkspaceCrates(index.root))) {
177
+ names.delete(name);
178
+ }
179
+ return names;
180
+ });
181
+ }
182
+
183
+ /** Package names the nearest package.json (up to the project root)
184
+ * declares, plus Node's builtin modules. */
185
+ function jsExternalPackages(index, filePath) {
186
+ let dir = path.dirname(filePath);
187
+ let manifest = null;
188
+ while (dir.startsWith(index.root)) {
189
+ const candidate = path.join(dir, 'package.json');
190
+ if (fs.existsSync(candidate)) { manifest = candidate; break; }
191
+ const parent = path.dirname(dir);
192
+ if (parent === dir) break;
193
+ dir = parent;
194
+ }
195
+ return memoized(index, `\x01npm\x00${manifest || ''}`, () => {
196
+ const names = new Set();
197
+ if (manifest) {
198
+ try {
199
+ const json = JSON.parse(fs.readFileSync(manifest, 'utf-8'));
200
+ for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
201
+ for (const name of Object.keys(json[field] || {})) names.add(name);
202
+ }
203
+ } catch { /* unreadable manifest: builtins only */ }
204
+ }
205
+ return names;
206
+ });
207
+ }
208
+
209
+ let nodeBuiltins = null;
210
+ function isNodeBuiltin(specifier) {
211
+ if (specifier.startsWith('node:')) return true;
212
+ if (!nodeBuiltins) nodeBuiltins = new Set(require('module').builtinModules);
213
+ return nodeBuiltins.has(specifier.split('/')[0]) || nodeBuiltins.has(specifier);
214
+ }
215
+
216
+ function jsPackageName(specifier) {
217
+ const parts = specifier.split('/');
218
+ return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
219
+ }
220
+
221
+ /** Namespaces (C#) / packages (Java) the project itself declares. */
222
+ function projectNamespaces(index, language) {
223
+ return memoized(index, `\x01ns\x00${language}`, () => {
224
+ const names = new Set();
225
+ for (const [, fileEntry] of index.files) {
226
+ if (fileEntry.language !== language) continue;
227
+ for (const symbol of fileEntry.symbols || []) {
228
+ if (symbol.namespace) names.add(symbol.namespace);
229
+ }
230
+ }
231
+ return names;
232
+ });
233
+ }
234
+
235
+ /** Is some leading dotted prefix of `qualified` (the full path excluded) a
236
+ * namespace/package the project declares? `org.junit.Assert` is project
237
+ * code when `org.junit` is a project package; nested types keep their
238
+ * package prefix (`a.b.Outer.Inner`). */
239
+ function hasProjectNamespacePrefix(index, language, qualified) {
240
+ const names = projectNamespaces(index, language);
241
+ const segments = String(qualified).split('.');
242
+ for (let i = segments.length - 1; i > 0; i--) {
243
+ if (names.has(segments.slice(0, i).join('.'))) return true;
244
+ }
245
+ return false;
246
+ }
247
+
248
+ // ── Module externality ────────────────────────────────────────────────────
249
+
250
+ function rustRootIsLocal(index, fileEntry, filePath, root, exceptBinding) {
251
+ if ((fileEntry.importBindings || []).some(binding => binding !== exceptBinding &&
252
+ (binding.alias || binding.name) === root)) return true;
253
+ if ((index.symbols.get(root) || []).some(definition =>
254
+ definition.file === filePath && definition.type === 'module')) return true;
255
+ // A sibling module file; the file system is read once per directory
256
+ // and name while the index lives (fix #403: twice per path per query).
257
+ const dir = path.dirname(filePath);
258
+ return memoized(index, `\x01rsmod\x00${dir}\x00${root}`, () =>
259
+ fs.existsSync(path.join(dir, `${root}.rs`)) || fs.existsSync(path.join(dir, root, 'mod.rs')));
260
+ }
261
+
262
+ /** Does a Rust path (`std::fs`, `tokio_util::sync`) name an external crate? */
263
+ function rustPathIsExternal(index, fileEntry, filePath, pathText, exceptBinding) {
264
+ const root = String(pathText || '').split('::').filter(Boolean)[0];
265
+ if (!root || root === 'crate' || root === 'self' || root === 'super' || root === 'Self') return false;
266
+ if (rustRootIsLocal(index, fileEntry, filePath, root, exceptBinding)) {
267
+ // `use std::sync; sync::Mutex` - the root is itself a binding.
268
+ const binding = (fileEntry.importBindings || []).find(candidate =>
269
+ candidate !== exceptBinding && (candidate.alias || candidate.name) === root &&
270
+ candidate.name !== '*');
271
+ return !!binding && binding.module !== pathText &&
272
+ rustPathIsExternal(index, fileEntry, filePath, binding.module, binding);
273
+ }
274
+ if (langTraits('rust').standardPathRoots.includes(root)) return true;
275
+ return rustExternalCrates(index, filePath).has(root);
276
+ }
277
+
278
+ /**
279
+ * Does an import binding of this file bind its name to an external module?
280
+ * Only positive evidence: resolver gaps, relative paths, project package
281
+ * names and anything unmodelled answer false.
282
+ */
283
+ function bindingIsExternal(index, fileEntry, filePath, binding) {
284
+ // A binding belongs to one file: its verdict is asked per site (fix #403).
285
+ if (!binding || typeof binding !== 'object') return bindingIsExternalOf(index, fileEntry, filePath, binding);
286
+ const memo = memoized(index, '\x01binding-external', () => new WeakMap());
287
+ if (memo.has(binding)) return memo.get(binding);
288
+ const verdict = bindingIsExternalOf(index, fileEntry, filePath, binding);
289
+ memo.set(binding, verdict);
290
+ return verdict;
291
+ }
292
+
293
+ function bindingIsExternalOf(index, fileEntry, filePath, binding) {
294
+ const module = String(binding?.module || '');
295
+ if (!module || binding.dynamic) return false;
296
+ switch (fileEntry.language) {
297
+ case 'rust':
298
+ return rustPathIsExternal(index, fileEntry, filePath, module, binding);
299
+ case 'java': {
300
+ if (binding.name === '*') return false;
301
+ if (hasProjectNamespacePrefix(index, 'java', module)) return false;
302
+ const suffix = `/${module.split('.').join('/')}.java`;
303
+ for (const file of index.files.keys()) if (file.endsWith(suffix)) return false;
304
+ // A package no project file declares: the standard library is
305
+ // external; anything else may be a resolver gap (generated
306
+ // sources) and is never exclusion evidence (fix #353).
307
+ return standardRooted('java', module) ? true : 'unresolved';
308
+ }
309
+ case 'csharp': {
310
+ // An alias target resolves from the namespace the directive sits
311
+ // in (fix #395), through an alias of an enclosing scope (#401).
312
+ const target = csharpResolveUsingName(index,
313
+ csharpAliasExpandedModule(fileEntry, binding), binding.namespace);
314
+ if (!target.includes('.') || hasProjectNamespacePrefix(index, 'csharp', target)) return false;
315
+ return standardRooted('csharp', target) ? true : 'unresolved';
316
+ }
317
+ case 'go': {
318
+ const goModule = findGoModule(path.dirname(filePath));
319
+ if (!goModule) return false;
320
+ const inside = prefix => module === prefix || module.startsWith(`${prefix}/`);
321
+ if (inside(goModule.modulePath)) return false;
322
+ if ((goModule.replaces || []).some(entry => inside(entry.from))) return false;
323
+ return true;
324
+ }
325
+ case 'javascript': case 'typescript': case 'tsx': {
326
+ if (fileEntry.moduleResolved?.[module]) return false;
327
+ if (/^[./~#]/.test(module) || module.startsWith('@/')) return false;
328
+ if (isNodeBuiltin(module)) return true;
329
+ return jsExternalPackages(index, filePath).has(jsPackageName(module));
330
+ }
331
+ case 'python': {
332
+ if (fileEntry.moduleResolved?.[module] || module.startsWith('.')) return false;
333
+ const { _unresolvedModuleIsGap } = require('./callers');
334
+ return !_unresolvedModuleIsGap(index, module, binding);
335
+ }
336
+ default:
337
+ return false;
338
+ }
339
+ }
340
+
341
+ function standardRooted(language, qualified) {
342
+ const root = String(qualified).split(/::|\./)[0];
343
+ return (langTraits(language)?.standardPathRoots || []).includes(root);
344
+ }
345
+
346
+ /** The name an import binding gives the imported entity at its origin. */
347
+ function bindingOriginalName(fileEntry, binding) {
348
+ if (fileEntry.language === 'csharp') return String(binding.module).split('.').pop();
349
+ return binding.name;
350
+ }
351
+
352
+ // ── Denotation ────────────────────────────────────────────────────────────
353
+
354
+ /**
355
+ * Does the type name `name` (optionally written under `qualifier`) denote an
356
+ * external type in `filePath`? Returns { original, via } (the external
357
+ * type's own name and the evidence) or null (project, unknown, or no
358
+ * evidence - the caller keeps its own resolution).
359
+ */
360
+ function externalTypeDenotation(index, filePath, name, qualifier, line, options = {}) {
361
+ if (!name) return null;
362
+ const scoped = rustScopedLine(index, filePath, line, name);
363
+ // `unscoped`: the site's spelling of the name is unknown (the parser kept
364
+ // only the last segment), so package/namespace scoping cannot apply.
365
+ const unscoped = options.unscoped === true;
366
+ const key = `${filePath}\x00${name}\x00${qualifier || ''}\x00${scoped ?? ''}\x00${unscoped ? 1 : 0}`;
367
+ return memoized(index, key, () => {
368
+ const fileEntry = index.files.get(filePath);
369
+ if (!fileEntry) return null;
370
+ const language = fileEntry.language;
371
+ if (qualifier) return qualifiedDenotation(index, fileEntry, filePath, name, qualifier);
372
+ // A local definition of the name decides (shadows imports). Rust
373
+ // items are visible in their own module only: an inline `mod rand
374
+ // { use std::..::AtomicU32; }` does not see the file's own struct.
375
+ const moduleOf = rustLine => innermostScope(rustModuleScopes(fileEntry), rustLine);
376
+ const siteModule = language === 'rust' && line != null ? moduleOf(line) : undefined;
377
+ if ((index.symbols.get(name) || []).some(definition => definition.file === filePath &&
378
+ (TYPE_KINDS.has(definition.type) || definition.type === 'type') &&
379
+ (siteModule === undefined || moduleOf(definition.startLine) === siteModule))) return null;
380
+ let bindings = (fileEntry.importBindings || []).filter(binding =>
381
+ binding.name !== '*' && (binding.alias || binding.name) === name && binding.module);
382
+ if (language === 'rust' && (bindings.length > 1 || line != null)) bindings = rustBindingsInScope(fileEntry, bindings, line);
383
+ if (bindings.length > 0) {
384
+ const verdicts = bindings.map(binding => bindingIsExternal(index, fileEntry, filePath, binding));
385
+ if (verdicts.some(verdict => !verdict)) return null;
386
+ const originals = new Set(bindings.map(binding => bindingOriginalName(fileEntry, binding)));
387
+ if (originals.size !== 1) return null;
388
+ const original = [...originals][0];
389
+ // An import names ONE fully qualified type (fix #372): even when
390
+ // its package is unresolvable (a possible generated-source gap),
391
+ // a project type whose package cannot prefix that name is not it.
392
+ const settled = verdict => verdict === true ||
393
+ (langTraits(language)?.typeNameScoping &&
394
+ bindings.every(binding => !fqnMayName(index, language, original, binding.module)));
395
+ return { original, via: bindings[0].module,
396
+ ...(!verdicts.every(settled) && { certain: false }) };
397
+ }
398
+ // Java/C#: the name reaches the file through its package/namespace
399
+ // and on-demand imports (fix #372).
400
+ const scoping = langTraits(language)?.typeNameScoping;
401
+ if (scoping) return unscoped ? null : scopedTypeDenotation(index, fileEntry, filePath, name, line, scoping);
402
+ // Rust names reach a file only through `use` (named or glob): when
403
+ // every glob is an external module, the name is not a project type.
404
+ if (language === 'rust') {
405
+ const globs = (fileEntry.importDetails || []).filter(detail =>
406
+ detail.type === 'use-glob' && detail.module);
407
+ if (globs.length > 0 && globs.every(glob =>
408
+ rustPathIsExternal(index, fileEntry, filePath, glob.module, null))) {
409
+ return { original: name, via: `${globs[0].module}::*` };
410
+ }
411
+ }
412
+ return null;
413
+ });
414
+ }
415
+
416
+ // ── Java / C# name scoping (fix #372) ─────────────────────────────────────
417
+
418
+ /**
419
+ * An unqualified type name with no declaration in the file and no
420
+ * single-name import, resolved by the language's scoping rules against the
421
+ * PROJECT types of that name:
422
+ * Java (JLS 6.4.1, 7.5): member types, the compilation unit, single-type
423
+ * imports, the same package, then on-demand imports together with the
424
+ * implicit `java.lang.*` (two on-demand sources of one name are ambiguous).
425
+ * C# (spec 7.6.5): member types, then for each enclosing namespace from
426
+ * the innermost outwards its members and its using directives; global
427
+ * usings (source and project-file <Using> items) apply everywhere. Using
428
+ * directives are taken file-wide, which can only keep a project type in
429
+ * scope (never exclude one).
430
+ * Returns { original, via, certain? } when no project type of that name is in
431
+ * scope (the name is external), { certain: false } when a project type and
432
+ * the implicit imports both supply it (ambiguous), and null when a project
433
+ * type is in scope or anything is unmodelled (nested or inherited member
434
+ * types, a field of that name, an unknown enclosing namespace).
435
+ */
436
+ function scopedTypeDenotation(index, fileEntry, filePath, name, line, scoping) {
437
+ const language = fileEntry.language;
438
+ const definitions = (index.symbols.get(name) || []).filter(definition =>
439
+ index.files.get(definition.file)?.language === language);
440
+ const projectTypes = definitions.filter(definition => TYPE_KINDS.has(definition.type));
441
+ if (projectTypes.length === 0) return null;
442
+ // Member types (own, inherited, statically imported) precede package
443
+ // scope; a field or property of that name obscures the type.
444
+ if (projectTypes.some(definition => definition.isNested || definition.enclosingType ||
445
+ definition.className)) return null;
446
+ if (definitions.some(definition => definition.type === 'field' || definition.type === 'property')) return null;
447
+ if (projectTypes.some(definition => !definition.namespace)) return null; // unnamed package / global namespace
448
+ // A site outside any recorded package/namespace is unknown, not the
449
+ // global namespace: preprocessor branches can hide a C# namespace
450
+ // declaration from the parse.
451
+ const siteNamespace = siteNamespaceOf(fileEntry, line);
452
+ if (!siteNamespace) return null;
453
+ const onDemand = (fileEntry.importDetails || [])
454
+ .filter(detail => detail.module && (detail.names || []).includes('*'))
455
+ .map(detail => String(detail.module).replace(/\.\*$/, ''));
456
+ const inScope = new Set();
457
+ if (scoping === 'package-on-demand') {
458
+ inScope.add(siteNamespace);
459
+ const onDemandProject = projectTypes.some(definition => onDemand.includes(definition.namespace));
460
+ if (projectTypes.some(definition => definition.namespace === siteNamespace)) return null;
461
+ const implicit = langTraits(language)?.implicitTypeImport;
462
+ const implicitHas = !!implicit?.names?.has(name);
463
+ if (onDemandProject) {
464
+ return implicitHas ? { original: name, via: `${implicit.module}.${name}`, certain: false } : null;
465
+ }
466
+ if (implicitHas) return { original: name, via: `${implicit.module}.${name}` };
467
+ const external = onDemand.filter(module => !hasProjectNamespacePrefix(index, language, `${module}.${name}`) &&
468
+ !projectNamespaces(index, language).has(module));
469
+ if (external.length === 0) return null;
470
+ return { original: name, via: `${external[0]}.*`,
471
+ ...(!external.every(module => standardRooted(language, module)) && { certain: false }) };
472
+ }
473
+ // namespace-usings
474
+ const enclosing = [];
475
+ for (let parts = siteNamespace ? siteNamespace.split('.') : []; parts.length > 0; parts = parts.slice(0, -1)) {
476
+ enclosing.push(parts.join('.'));
477
+ }
478
+ if (projectTypes.some(definition => enclosing.includes(definition.namespace))) return null;
479
+ // Directives resolved the way C# resolves them (fix #395): `using
480
+ // Internal;` inside `namespace Acme.Tests` names Acme.Internal. A using
481
+ // alias spelled like the name, or an unreadable project file, is not
482
+ // modelled.
483
+ const scope = csharpUsings(index, filePath);
484
+ if (scope.unreadable || scope.aliases.has(name)) return null;
485
+ // `using static T` also brings T's nested types (already abstained above).
486
+ const usings = new Set([...scope.namespaces, ...scope.statics]);
487
+ if (projectTypes.some(definition => usings.has(definition.namespace))) return null;
488
+ const external = [...usings].filter(module => !csharpDeclaredNames(index).namespaces.has(module));
489
+ return { original: name, via: external.length > 0 ? `${external.sort()[0]}.${name}` : name };
490
+ }
491
+
492
+ /** Could a project type named `name` be the type `fqn` names? Only a type
493
+ * in a named package/namespace that prefixes it (nested types included). */
494
+ function fqnMayName(index, language, name, fqn) {
495
+ return (index.symbols.get(name) || []).some(definition => TYPE_KINDS.has(definition.type) &&
496
+ index.files.get(definition.file)?.language === language &&
497
+ !!definition.namespace && String(fqn).startsWith(`${definition.namespace}.`));
498
+ }
499
+
500
+ /** The package (Java) or innermost enclosing namespace (C#) at `line`:
501
+ * the namespace of the innermost type declared around it, else the one
502
+ * namespace every type of the file shares; undefined when unknown. */
503
+ function siteNamespaceOf(fileEntry, line) {
504
+ const types = (fileEntry.symbols || []).filter(symbol => TYPE_KINDS.has(symbol.type));
505
+ if (line != null) {
506
+ const around = innermostScope(types.filter(symbol => symbol.endLine >= symbol.startLine), line);
507
+ if (around) return around.namespace || '';
508
+ }
509
+ const namespaces = new Set(types.map(symbol => symbol.namespace || ''));
510
+ return namespaces.size === 1 ? [...namespaces][0] : undefined;
511
+ }
512
+
513
+ // ── C# using directives (fix #395) ───────────────────────────────────────
514
+
515
+ /** Namespaces the C# project declares (every leading prefix of a declared
516
+ * namespace is one too) and the fully qualified names of its types. */
517
+ function csharpDeclaredNames(index) {
518
+ return memoized(index, '\x01cs-declared', () => {
519
+ const declared = new Set();
520
+ const types = new Set();
521
+ for (const [, fileEntry] of index.files) {
522
+ if (fileEntry.language !== 'csharp') continue;
523
+ for (const symbol of fileEntry.symbols || []) {
524
+ if (!TYPE_KINDS.has(symbol.type) && symbol.type !== 'delegate') continue;
525
+ const namespace = symbol.namespace || '';
526
+ if (namespace) declared.add(namespace);
527
+ const outer = symbol.enclosingType ? `${symbol.enclosingType}.` : '';
528
+ types.add(`${namespace ? `${namespace}.` : ''}${outer}${symbol.name}`);
529
+ }
530
+ }
531
+ const namespaces = new Set();
532
+ for (const namespace of declared) {
533
+ for (let at = namespace.indexOf('.'); at !== -1; at = namespace.indexOf('.', at + 1)) {
534
+ namespaces.add(namespace.slice(0, at));
535
+ }
536
+ namespaces.add(namespace);
537
+ }
538
+ return { namespaces, types };
539
+ });
540
+ }
541
+
542
+ /**
543
+ * The namespace or type a C# using directive names (spec 14.5.2): written
544
+ * inside `namespace A.B`, its first identifier binds in A.B, then A, then the
545
+ * global namespace, the first that declares a namespace or type of that name
546
+ * winning. `global using` directives, project-file <Using> items and
547
+ * `global::` spellings are resolved from the global namespace. A name no
548
+ * enclosing namespace declares keeps its spelling (an external namespace).
549
+ */
550
+ function csharpResolveUsingName(index, written, enclosing) {
551
+ const text = String(written || '').replace(/\s+/g, '').replace(/^global::/, '');
552
+ if (!enclosing || !text) return text;
553
+ const first = text.split('.')[0].replace(/<.*$/s, '');
554
+ const { namespaces, types } = csharpDeclaredNames(index);
555
+ for (let parts = String(enclosing).split('.'); parts.length > 0; parts = parts.slice(0, -1)) {
556
+ const candidate = `${parts.join('.')}.${first}`;
557
+ if (namespaces.has(candidate) || types.has(candidate)) return `${parts.join('.')}.${text}`;
558
+ }
559
+ return text;
560
+ }
561
+
562
+ /**
563
+ * A using alias directive's written target with a leading alias declared in
564
+ * an enclosing scope substituted (fix #401): `using W = LC.Widget;` inside
565
+ * `namespace App` after a compilation-unit `using LC = Lib.Core;`.
566
+ */
567
+ function csharpAliasExpandedModule(fileEntry, binding) {
568
+ let module = String(binding?.module || '');
569
+ const scope = binding?.namespace || '';
570
+ for (let hop = 0; hop < 4; hop++) {
571
+ const [head, ...rest] = module.split('.');
572
+ const outer = (fileEntry?.importBindings || []).find(other => other !== binding &&
573
+ other.kind === 'using' && other.name === head && other.module &&
574
+ (other.namespace || '') !== scope &&
575
+ ((other.namespace || '') === '' || scope.startsWith(`${other.namespace}.`)));
576
+ if (!outer || rest.length === 0) break;
577
+ module = [outer.module, ...rest].join('.');
578
+ }
579
+ return module;
580
+ }
581
+
582
+ /** A directive record's target with its generic arguments removed. */
583
+ function csharpUsingTarget(text) {
584
+ return String(text || '').replace(/<.*$/s, '');
585
+ }
586
+
587
+ function csharpAddDirective(scope, index, { module, names, static: isStatic, namespace }) {
588
+ const target = csharpUsingTarget(csharpResolveUsingName(index, module, namespace));
589
+ if (!target) return;
590
+ const alias = (names || []).find(name => name && name !== '*');
591
+ if (alias) {
592
+ scope.aliases.set(alias, target);
593
+ if (!scope.aliasScopes) scope.aliasScopes = new Map();
594
+ scope.aliasScopes.set(alias, namespace || '');
595
+ }
596
+ else if (isStatic) scope.statics.add(target);
597
+ else scope.namespaces.add(target);
598
+ }
599
+
600
+ /**
601
+ * Using directives in effect in a C# file (fix #395): the file's own
602
+ * directives (resolved from the namespace each is written in), `global
603
+ * using` directives of every source file, and the <Using> items of the
604
+ * project files above it (`csharpProjectFileUsings`).
605
+ * Returns { namespaces: Set, statics: Set (type names), aliases: Map
606
+ * (alias -> target), unreadable } - unreadable when a project file could not
607
+ * be read (its items are unknown).
608
+ */
609
+ function csharpUsings(index, filePath) {
610
+ return memoized(index, `\x01cs-usings\x00${filePath}`, () => {
611
+ const scope = { namespaces: new Set(), statics: new Set(), aliases: new Map(), unreadable: false };
612
+ const globals = memoized(index, '\x01cs-global-usings', () => {
613
+ const out = [];
614
+ for (const [, entry] of index.files) {
615
+ if (entry.language !== 'csharp') continue;
616
+ for (const detail of entry.importDetails || []) {
617
+ if (detail.global && detail.module) out.push(detail);
618
+ }
619
+ }
620
+ return out;
621
+ });
622
+ for (const detail of globals) csharpAddDirective(scope, index, detail);
623
+ const fileEntry = index.files.get(filePath);
624
+ for (const detail of fileEntry?.importDetails || []) {
625
+ if (detail.type === 'using' && detail.module && !detail.global) {
626
+ csharpAddDirective(scope, index, detail);
627
+ }
628
+ }
629
+ for (const item of csharpProjectFileUsings(index, filePath)) {
630
+ if (item === null) { scope.unreadable = true; continue; }
631
+ csharpAddDirective(scope, index, item);
632
+ }
633
+ csharpExpandAliasTargets(scope);
634
+ return scope;
635
+ });
636
+ }
637
+
638
+ /**
639
+ * A using alias's target may begin with an alias declared in an enclosing
640
+ * scope (fix #401: `using LC = Lib.Core;` in the compilation unit, then
641
+ * `namespace App { using W = LC.Widget; }`). Aliases of the same body never
642
+ * see each other (C# spec, using alias directives).
643
+ */
644
+ function csharpExpandAliasTargets(scope) {
645
+ const scopes = scope.aliasScopes;
646
+ if (!scopes || scope.aliases.size < 2) return;
647
+ const encloses = (outer, inner) => outer !== inner && (outer === '' || inner.startsWith(`${outer}.`));
648
+ for (let pass = 0; pass < 4; pass++) {
649
+ let changed = false;
650
+ for (const [alias, target] of scope.aliases) {
651
+ const [head, ...rest] = String(target).split('.');
652
+ if (head === alias || !scope.aliases.has(head)) continue;
653
+ if (!encloses(scopes.get(head) ?? '', scopes.get(alias) ?? '')) continue;
654
+ scope.aliases.set(alias, [scope.aliases.get(head), ...rest].join('.'));
655
+ changed = true;
656
+ }
657
+ if (!changed) break;
658
+ }
659
+ }
660
+
661
+ /** The directory whose `.git` marks the repository holding `dir`, or null. */
662
+ function gitTopOf(index, dir) {
663
+ return memoized(index, `\x01git-top\x00${dir}`, () => {
664
+ for (let current = dir; ; current = path.dirname(current)) {
665
+ try { if (fs.existsSync(path.join(current, '.git'))) return current; } catch { /* keep walking */ }
666
+ if (current === path.dirname(current)) return null;
667
+ }
668
+ });
669
+ }
670
+
671
+ /**
672
+ * <Using> items of the MSBuild project files above a C# file: the .csproj
673
+ * and Directory.Build.props/targets of every directory from the file's up
674
+ * to the index root, and on up to the repository root when the index root
675
+ * lies inside one (fix #395: MSBuild imports Directory.Build.props from
676
+ * ancestor directories, so a sub-project indexed on its own still sees the
677
+ * repository's usings). `<Using Include="X"/>` is `global using X;`,
678
+ * `Static="true"` a `global using static`, `Alias="Z"` a `global using Z =`;
679
+ * null marks a file that could not be read.
680
+ */
681
+ function csharpProjectFileUsings(index, filePath) {
682
+ const dirs = [];
683
+ const root = path.resolve(index.root);
684
+ let dir = path.dirname(filePath);
685
+ for (; ; dir = path.dirname(dir)) {
686
+ dirs.push(dir);
687
+ if (dir === root || !dir.startsWith(root) || dir === path.dirname(dir)) break;
688
+ }
689
+ if (dir === root) {
690
+ const top = gitTopOf(index, root);
691
+ if (top && top !== root && root.startsWith(top + path.sep)) {
692
+ for (let up = path.dirname(root); ; up = path.dirname(up)) {
693
+ dirs.push(up);
694
+ if (up === top || up === path.dirname(up)) break;
695
+ }
696
+ }
697
+ }
698
+ return dirs.flatMap(current => memoized(index, `\x01cs-proj-usings\x00${current}`, () => {
699
+ let names;
700
+ try { names = fs.readdirSync(current); } catch { return []; }
701
+ const out = [];
702
+ for (const file of names.sort()) {
703
+ if (!/\.csproj$|^Directory\.Build\.(props|targets)$/.test(file)) continue;
704
+ let text;
705
+ try { text = fs.readFileSync(path.join(current, file), 'utf-8'); } catch { out.push(null); continue; }
706
+ for (const match of text.matchAll(/<Using\b([^>]*)>/g)) {
707
+ const include = /\bInclude\s*=\s*"([^"]+)"/.exec(match[1]);
708
+ if (!include) continue;
709
+ const alias = /\bAlias\s*=\s*"([^"]+)"/.exec(match[1]);
710
+ out.push({
711
+ module: include[1].trim(),
712
+ names: alias ? [alias[1].trim()] : ['*'],
713
+ ...(/\bStatic\s*=\s*"true"/i.test(match[1]) && { static: true }),
714
+ });
715
+ }
716
+ }
717
+ return out;
718
+ }));
719
+ }
720
+
721
+ /** Namespaces every C# file sees through `global using` directives and
722
+ * project-file items, plus null when a project file is unreadable. */
723
+ function csharpGlobalUsings(index, filePath) {
724
+ const scope = csharpUsings(index, filePath);
725
+ return scope.unreadable ? [...scope.namespaces, null] : [...scope.namespaces];
726
+ }
727
+
728
+ // Rust `use` declarations are scoped to their module or block: an inline
729
+ // `mod rand { use std::sync::atomic::AtomicU32; }` binds nothing outside it.
730
+ // Both lists are read per site: kept per symbols array (a rebuilt file has a
731
+ // new one).
732
+ const rustScopeLists = new WeakMap();
733
+ function rustScopeListsOf(fileEntry) {
734
+ const symbols = fileEntry.symbols || [];
735
+ let lists = rustScopeLists.get(symbols);
736
+ if (!lists || lists.length !== symbols.length || lists.first !== symbols[0] ||
737
+ lists.last !== symbols[symbols.length - 1]) {
738
+ const spanning = symbols.filter(symbol => symbol.endLine > symbol.startLine);
739
+ lists = {
740
+ length: symbols.length,
741
+ first: symbols[0],
742
+ last: symbols[symbols.length - 1],
743
+ scopes: spanning.filter(symbol =>
744
+ symbol.type === 'module' || symbol.type === 'function' || symbol.type === 'method'),
745
+ modules: spanning.filter(symbol => symbol.type === 'module'),
746
+ };
747
+ rustScopeLists.set(symbols, lists);
748
+ }
749
+ return lists;
750
+ }
751
+
752
+ function rustScopes(fileEntry) {
753
+ return rustScopeListsOf(fileEntry).scopes;
754
+ }
755
+
756
+ function rustModuleScopes(fileEntry) {
757
+ return rustScopeListsOf(fileEntry).modules;
758
+ }
759
+
760
+ function innermostScope(scopes, line) {
761
+ let best = null;
762
+ for (const scope of scopes) {
763
+ if (scope.startLine <= line && scope.endLine >= line &&
764
+ (!best || scope.endLine - scope.startLine < best.endLine - best.startLine)) best = scope;
765
+ }
766
+ return best;
767
+ }
768
+
769
+ /** The site line as a memo key part: in a Rust file that declares or
770
+ * imports the name (its module and the `use` scopes around the site decide);
771
+ * otherwise the answer is the file's (fix #403: keyed per line, every
772
+ * `T::new()` site of a file recomputed it). */
773
+ function rustScopedLine(index, filePath, line, name) {
774
+ if (line == null) return null;
775
+ const fileEntry = index.files.get(filePath);
776
+ if (fileEntry?.language !== 'rust') return null;
777
+ // Per file and name (fix #405: every `T::new()` site asked again).
778
+ const scoped = memoized(index, `scoped\x00${filePath}\x00${name}`, () => {
779
+ const declares = (index.symbols.get(name) || []).some(definition => definition.file === filePath &&
780
+ (TYPE_KINDS.has(definition.type) || definition.type === 'type'));
781
+ return declares || (fileEntry.importBindings || []).some(binding =>
782
+ binding.name !== '*' && (binding.alias || binding.name) === name && binding.module);
783
+ });
784
+ return scoped ? line : null;
785
+ }
786
+
787
+ /** Bindings visible at `line`: those of the innermost enclosing scope that
788
+ * declares any (a scope's `use` shadows its parents'); none when the line is
789
+ * unknown and the bindings disagree in scope. */
790
+ function rustBindingsInScope(fileEntry, bindings, line) {
791
+ const scopes = rustScopes(fileEntry);
792
+ const scopeOf = binding => binding.line == null ? null : innermostScope(scopes, binding.line);
793
+ if (line == null) {
794
+ return new Set(bindings.map(scopeOf)).size === 1 ? bindings : [];
795
+ }
796
+ let best = null;
797
+ let visible = [];
798
+ for (const binding of bindings) {
799
+ const scope = scopeOf(binding);
800
+ if (scope && !(scope.startLine <= line && scope.endLine >= line)) continue;
801
+ const span = scope ? scope.endLine - scope.startLine : Infinity;
802
+ if (best === null || span < best) { best = span; visible = [binding]; }
803
+ else if (span === best) visible.push(binding);
804
+ }
805
+ return visible;
806
+ }
807
+
808
+ function qualifiedDenotation(index, fileEntry, filePath, name, qualifier) {
809
+ const language = fileEntry.language;
810
+ switch (language) {
811
+ case 'rust':
812
+ return rustPathIsExternal(index, fileEntry, filePath, qualifier, null)
813
+ ? { original: name, via: `${qualifier}::${name}` } : null;
814
+ case 'cpp': {
815
+ const root = String(qualifier).replace(/^::/, '').split('::')[0];
816
+ if (!langTraits(language)?.standardPathRoots?.includes(root)) return null;
817
+ // A project that declares the namespace itself (`namespace std`
818
+ // specializations) keeps its own resolution.
819
+ const declares = (index.symbols.get(name) || []).some(definition =>
820
+ String(definition.namespace || '').split('::')[0] === root);
821
+ return declares ? null : { original: name, via: `${qualifier}::${name}` };
822
+ }
823
+ case 'java': {
824
+ if (!/^[a-z_]/.test(qualifier)) return null;
825
+ const bindingIsType = (fileEntry.importBindings || []).some(binding =>
826
+ (binding.alias || binding.name) === qualifier.split('.')[0]);
827
+ if (bindingIsType) return null;
828
+ const verdict = bindingIsExternal(index, fileEntry, filePath, { module: `${qualifier}.${name}`, name });
829
+ if (!verdict) return null;
830
+ return { original: name, via: `${qualifier}.${name}`, ...(verdict !== true && { certain: false }) };
831
+ }
832
+ case 'javascript': case 'typescript': case 'tsx': case 'python': {
833
+ const bindings = (fileEntry.importBindings || []).filter(binding =>
834
+ (binding.alias || binding.name) === qualifier && binding.module);
835
+ if (bindings.length === 0) return null;
836
+ if (!bindings.every(binding =>
837
+ ['import', 'namespace', 'require', 'default'].includes(binding.kind))) return null;
838
+ return bindings.every(binding => bindingIsExternal(index, fileEntry, filePath, binding))
839
+ ? { original: name, via: `${bindings[0].module}.${name}` } : null;
840
+ }
841
+ default:
842
+ return null;
843
+ }
844
+ }
845
+
846
+ /**
847
+ * The type a call record names in writing, or null.
848
+ * { name, qualifier, receiverTyped } - receiverTyped marks a receiver whose
849
+ * TYPE (not the callee name) was written: annotation or constructor.
850
+ */
851
+ function siteWrittenType(index, fileEntry, call) {
852
+ const written = siteWrittenTypeRaw(index, fileEntry, call);
853
+ if (!written || written.qualifier || !langTraits(fileEntry.language)?.typeNameScoping ||
854
+ !projectTypeNamed(index, written.name)) return written;
855
+ // Java/C# (fix #372): an unqualified name is resolved by package or
856
+ // namespace scope, so the site's own spelling must be unqualified. The
857
+ // parser keeps the last segment of `new b.Thread()` / `b.Thread t`; the
858
+ // written text at the recorded span decides.
859
+ const span = written.receiverTyped
860
+ ? call.receiverTypeEvidence
861
+ : (call.isMethod ? null : call.callSite);
862
+ if (written.receiverTyped || !call.isMethod) {
863
+ const spelled = spelledTypeQualifier(index, fileEntry, span, written.name);
864
+ if (spelled === undefined) return { ...written, unscoped: true };
865
+ if (spelled) return { ...written, qualifier: spelled };
866
+ }
867
+ return written;
868
+ }
869
+
870
+ /** The qualifier a type name is spelled with at a recorded source span
871
+ * (`new b.Thread()` -> 'b', `Thread` -> null); undefined when unknown. */
872
+ function spelledTypeQualifier(index, fileEntry, span, name) {
873
+ if (!span || !Number.isInteger(span.start) || !Number.isInteger(span.end)) return undefined;
874
+ const filePath = path.join(index.root, fileEntry.relativePath);
875
+ let content;
876
+ try { content = index._readFile(filePath); } catch { return undefined; }
877
+ if (typeof content !== 'string') return undefined;
878
+ const text = content.slice(span.start, span.end).replace(/^\s*new\b/, '')
879
+ .replace(/[<([{].*$/s, '').replace(/\s+/g, '');
880
+ if (text === name) return null;
881
+ if (!text.endsWith(`.${name}`)) return undefined;
882
+ return text.slice(0, -name.length - 1).replace(/^global::/, '') || undefined;
883
+ }
884
+
885
+ function siteWrittenTypeRaw(index, fileEntry, call) {
886
+ const language = fileEntry.language;
887
+ if (!call.isMethod) {
888
+ // Constructor-shaped call: the callee name is a type name.
889
+ if (!call.isConstructor && !(index.symbols.get(call.name) || []).some(definition =>
890
+ TYPE_KINDS.has(definition.type))) return null;
891
+ return { name: call.name, qualifier: (call.isConstructor && call.receiver) || null, receiverTyped: false };
892
+ }
893
+ if (language === 'rust' && call.isPathCall && call.receiver && !call.receiverType) {
894
+ const segments = String(call.receiver).split('::').filter(Boolean);
895
+ const last = segments[segments.length - 1];
896
+ if (!last || !/^[A-Z]/.test(last) || last === 'Self') return null;
897
+ return { name: last, qualifier: segments.length > 1 ? segments.slice(0, -1).join('::') : null,
898
+ receiverTyped: false };
899
+ }
900
+ if (call.receiverIsTypeQualified && call.receiver && !call.receiverType &&
901
+ (language === 'java' || language === 'csharp')) {
902
+ return { name: call.receiver, qualifier: call.receiverTypeQualifier || null, receiverTyped: false };
903
+ }
904
+ if (call.receiverType && (call.receiverTypeSource === 'annotation' ||
905
+ call.receiverTypeSource === 'constructor')) {
906
+ // A C++ declaration's written namespace qualifies the declared type
907
+ // itself, not a pointee (`std::unique_ptr<file> p; p->f()`).
908
+ const namespace = call.receiverTypeNamespace &&
909
+ call.receiverTypeEvidence?.type === call.receiverType ? call.receiverTypeNamespace : null;
910
+ return { name: call.receiverType,
911
+ qualifier: call.receiverTypeQualifier || namespace || null, receiverTyped: true,
912
+ source: call.receiverTypeSource };
913
+ }
914
+ return null;
915
+ }
916
+
917
+ function ownerName(definition) {
918
+ if (TYPE_KINDS.has(definition.type)) return definition.name;
919
+ return definition.className || String(definition.receiver || '').replace(/^[*&]\s*/, '') || null;
920
+ }
921
+
922
+ /**
923
+ * What a target definition's owner type denotes: 'project' (a project type
924
+ * definition), 'external' (an impl/extension on the external type), or
925
+ * 'unknown'.
926
+ */
927
+ function ownerDenotation(index, definition, original) {
928
+ const key = `\x01owner\x00${definition.file}:${definition.startLine}:${definition.name}:${original}`;
929
+ return memoized(index, key, () => {
930
+ if (TYPE_KINDS.has(definition.type)) return 'project';
931
+ const owner = ownerName(definition);
932
+ if (definition.implSelfQualifier) {
933
+ return externalTypeDenotation(index, definition.file, owner, definition.implSelfQualifier, definition.startLine)
934
+ ? 'external' : 'project';
935
+ }
936
+ // A project type that declares the external type as an ancestor
937
+ // (`class File extends java.io.File`) receives its dispatch.
938
+ const ownerDefs = (index.symbols.get(owner) || []).filter(candidate =>
939
+ TYPE_KINDS.has(candidate.type));
940
+ if (ownerDefs.some(candidate => (index._getInheritanceParents?.(owner, candidate.file) || [])
941
+ .some(parent => String(parent).split(/::|\./).pop() === original))) return 'external';
942
+ if (ownerDefs.some(candidate => candidate.file === definition.file)) return 'project';
943
+ if (externalTypeDenotation(index, definition.file, owner, null, definition.startLine)) return 'external';
944
+ return ownerDefs.length > 0 ? 'project' : 'unknown';
945
+ });
946
+ }
947
+
948
+ function projectTypeNamed(index, name) {
949
+ return (index.symbols.get(name) || []).some(definition => TYPE_KINDS.has(definition.type));
950
+ }
951
+
952
+ /**
953
+ * The type a call record writes and what it denotes in its file. A record
954
+ * never changes once its file's calls are read, so the answer is kept per
955
+ * record object while the index lives (fix #405b: orientation refines every
956
+ * definition of a name over the same records, and each refinement asked
957
+ * again per record).
958
+ */
959
+ function siteDenotation(index, filePath, fileEntry, call) {
960
+ const memos = memoOf(index);
961
+ let memo = memos.get('\x01site');
962
+ if (!memo) memos.set('\x01site', memo = new WeakMap());
963
+ let site = memo.get(call);
964
+ if (site === undefined) {
965
+ const written = siteWrittenType(index, fileEntry, call);
966
+ site = {
967
+ written,
968
+ denotation: written ? externalTypeDenotation(index, filePath, written.name, written.qualifier,
969
+ call.line, { unscoped: written.unscoped === true }) : null,
970
+ };
971
+ memo.set(call, site);
972
+ }
973
+ return site;
974
+ }
975
+
976
+ /**
977
+ * Verdict for a call site against candidate target definitions (the pinned
978
+ * targets on the caller side, every same-name callable on the callee side).
979
+ * Returns null (the gate does not apply) or one of:
980
+ * { verdict: 'exclude', reason: 'external-receiver', via }
981
+ * { verdict: 'dispatch', via } an impl on the external type may supply it
982
+ * { verdict: 'strip', via } receiver typed by the external name: the
983
+ * engine resolves it without that type
984
+ */
985
+ function externalTypeGate(index, filePath, fileEntry, call, targetsOrThunk) {
986
+ if (!fileEntry || !targetsOrThunk) return null;
987
+ // Go: a receiver typed by an external package's type may be an interface
988
+ // every project type with the method set satisfies; the existing
989
+ // package-qualified physics routes it (possible dispatch).
990
+ if (fileEntry.language === 'go') return null;
991
+ const { written, denotation } = siteDenotation(index, filePath, fileEntry, call);
992
+ if (!denotation) return null;
993
+ const { original, via } = denotation;
994
+ // Only a real collision needs the gate: without a project type of that
995
+ // name the engine's own resolution cannot conflate the two.
996
+ if (!projectTypeNamed(index, original)) return null;
997
+ const targets = typeof targetsOrThunk === 'function' ? targetsOrThunk() : targetsOrThunk;
998
+ if (!targets || targets.length === 0) return null;
999
+ const collision = [];
1000
+ const others = [];
1001
+ for (const target of targets) (ownerName(target) === original ? collision : others).push(target);
1002
+ // An unresolvable non-standard package: the name is not the project
1003
+ // type's, but a resolver gap cannot be ruled out - never confirmed,
1004
+ // never excluded.
1005
+ if (denotation.certain === false) return collision.length > 0 ? { verdict: 'unverified', via } : null;
1006
+ // A target the same-name PROJECT type reaches (its ancestor, a trait it
1007
+ // implements, a blanket impl) is where the engine could conflate the two
1008
+ // types; the external type may reach it too, so it stays visible.
1009
+ const related = written.receiverTyped
1010
+ ? others.filter(target => reachableThroughNamesake(index, target, original)) : [];
1011
+ if (collision.length === 0) return related.length > 0 ? { verdict: 'dispatch', via } : null;
1012
+ let external = 0;
1013
+ for (const target of collision) {
1014
+ const verdict = ownerDenotation(index, target, original);
1015
+ if (verdict === 'external') external++;
1016
+ else if (verdict !== 'project') return written.receiverTyped ? { verdict: 'strip', via } : null;
1017
+ }
1018
+ if (external > 0) return { verdict: 'dispatch', via };
1019
+ // Structural typing: an annotated external type admits any value of the
1020
+ // right shape, so only a constructed receiver is exact.
1021
+ if (written.receiverTyped && written.source !== 'constructor' &&
1022
+ langTraits(fileEntry.language)?.typeSystem === 'structural') return { verdict: 'dispatch', via };
1023
+ return related.length > 0
1024
+ ? { verdict: 'strip', via }
1025
+ : { verdict: 'exclude', reason: 'external-receiver', via };
1026
+ }
1027
+
1028
+ /**
1029
+ * Verdict for a receiver whose declared type (a field, written in its own
1030
+ * file) denotes the external type `original`: 'exclude' when no target can
1031
+ * be reached by a value of that type, 'dispatch' when an impl on the
1032
+ * external type, a blanket impl, an extension method or a trait it may
1033
+ * implement could supply one, 'unknown' otherwise. Structural typing admits
1034
+ * any value of the right shape: always 'dispatch'.
1035
+ */
1036
+ function externalReceiverVerdict(index, language, targets, original) {
1037
+ if (!targets || targets.length === 0) return 'unknown';
1038
+ if (langTraits(language)?.typeSystem !== 'nominal' || language === 'go') return 'dispatch';
1039
+ let dispatch = false;
1040
+ for (const target of targets) {
1041
+ const owner = ownerName(target);
1042
+ if (!owner) continue;
1043
+ if (owner === original) {
1044
+ const verdict = ownerDenotation(index, target, original);
1045
+ if (verdict === 'external') dispatch = true;
1046
+ else if (verdict !== 'project') return 'unknown';
1047
+ continue;
1048
+ }
1049
+ if (target.isExtensionMethod || reachableByExternal(index, target, original)) dispatch = true;
1050
+ }
1051
+ return dispatch ? 'dispatch' : 'exclude';
1052
+ }
1053
+
1054
+ /**
1055
+ * Can a value of the external type `name` reach `target`? A blanket impl
1056
+ * member; a trait member that some impl for `name`, or a blanket impl,
1057
+ * provides; or a member of a project type that implements / extends the
1058
+ * external type itself (`class MyList implements java.util.List`, `impl
1059
+ * std::io::Write for MyWriter`) - the value may be such an instance.
1060
+ */
1061
+ function reachableByExternal(index, target, name) {
1062
+ const owner = ownerName(target);
1063
+ if (target.ownerGenerics && genericParamNames(target.ownerGenerics).has(owner)) return true;
1064
+ if (target.traitName === name) return true;
1065
+ const key = `\x01extreach\x00${target.file}:${target.startLine}:${target.name}\x00${name}`;
1066
+ return memoized(index, key, () => {
1067
+ const sameName = index.symbols.get(target.name) || [];
1068
+ if (sameName.some(candidate => candidate.traitName === owner && (candidate.className === name ||
1069
+ (candidate.ownerGenerics && genericParamNames(candidate.ownerGenerics).has(candidate.className))))) {
1070
+ return true;
1071
+ }
1072
+ const seen = new Set([owner]);
1073
+ let level = (index.symbols.get(owner) || []).filter(d => TYPE_KINDS.has(d.type));
1074
+ for (let depth = 0; depth < 8 && level.length > 0; depth++) {
1075
+ const next = [];
1076
+ for (const definition of level) {
1077
+ for (const parent of index._getInheritanceParents?.(definition.name, definition.file) || []) {
1078
+ const parentName = String(parent).replace(/<.*$/s, '').split(/::|\./).pop();
1079
+ if (parentName === name) return true;
1080
+ if (seen.has(parentName)) continue;
1081
+ seen.add(parentName);
1082
+ next.push(...(index.symbols.get(parentName) || []).filter(d => TYPE_KINDS.has(d.type)));
1083
+ }
1084
+ }
1085
+ level = next;
1086
+ }
1087
+ return false;
1088
+ });
1089
+ }
1090
+
1091
+ function genericParamNames(text) {
1092
+ const inner = String(text || '').trim().replace(/^</, '').replace(/>$/, '');
1093
+ const names = new Set();
1094
+ let depth = 0;
1095
+ let start = 0;
1096
+ const parts = [];
1097
+ for (let i = 0; i < inner.length; i++) {
1098
+ const ch = inner[i];
1099
+ if (ch === '<' || ch === '(' || ch === '[') depth++;
1100
+ else if (ch === '>' || ch === ')' || ch === ']') depth--;
1101
+ else if (ch === ',' && depth === 0) { parts.push(inner.slice(start, i)); start = i + 1; }
1102
+ }
1103
+ parts.push(inner.slice(start));
1104
+ for (const part of parts) {
1105
+ const match = part.trim().match(/^([A-Za-z_][A-Za-z0-9_]*)/);
1106
+ if (match && !part.trim().startsWith("'")) names.add(match[1]);
1107
+ }
1108
+ return names;
1109
+ }
1110
+
1111
+ /**
1112
+ * Could a value of the project type named `name` reach `target` other than
1113
+ * as its owner: a blanket impl member (generic owner), a trait/interface the
1114
+ * type implements, or an ancestor class? Name-level, bounded.
1115
+ */
1116
+ function reachableThroughNamesake(index, target, name) {
1117
+ const owner = ownerName(target);
1118
+ if (!owner) return false;
1119
+ const key = `\x01reach\x00${target.file}:${target.startLine}:${target.name}\x00${name}`;
1120
+ return memoized(index, key, () => {
1121
+ if (target.ownerGenerics && genericParamNames(target.ownerGenerics).has(owner)) return true;
1122
+ if ((index.symbols.get(target.name) || []).some(candidate =>
1123
+ candidate.className === name && candidate.traitName === owner)) return true;
1124
+ const seen = new Set([name]);
1125
+ let level = (index.symbols.get(name) || []).filter(d => TYPE_KINDS.has(d.type));
1126
+ for (let depth = 0; depth < 8 && level.length > 0; depth++) {
1127
+ const next = [];
1128
+ for (const definition of level) {
1129
+ for (const parent of index._getInheritanceParents?.(definition.name, definition.file) || []) {
1130
+ const parentName = String(parent).replace(/<.*$/s, '').split(/::|\./).pop();
1131
+ if (parentName === owner) return true;
1132
+ if (seen.has(parentName)) continue;
1133
+ seen.add(parentName);
1134
+ next.push(...(index.symbols.get(parentName) || []).filter(d => TYPE_KINDS.has(d.type)));
1135
+ }
1136
+ }
1137
+ level = next;
1138
+ }
1139
+ return false;
1140
+ });
1141
+ }
1142
+
1143
+ /**
1144
+ * A producer call whose written type (type-qualified path / static factory /
1145
+ * constructor) denotes an external type that shares its name with a project
1146
+ * type: the external evidence string, else null. The value it produces was
1147
+ * decided outside the project.
1148
+ */
1149
+ function externalProducerVia(index, filePath, fileEntry, call) {
1150
+ if (!fileEntry) return null;
1151
+ const written = siteWrittenType(index, fileEntry, call);
1152
+ if (!written || written.receiverTyped) return null;
1153
+ const denotation = externalTypeDenotation(index, filePath, written.name, written.qualifier, call.line,
1154
+ { unscoped: written.unscoped === true });
1155
+ if (!denotation || denotation.certain === false || !projectTypeNamed(index, denotation.original)) return null;
1156
+ return `${denotation.via}${fileEntry.language === 'rust' || fileEntry.language === 'cpp' ? '::' : '.'}${call.name}`;
1157
+ }
1158
+
1159
+ /** A copy of the call whose external-typed receiver no longer names a type. */
1160
+ function stripExternalReceiverType(call, via) {
1161
+ const stripped = { ...call, receiverExternalFlow: via };
1162
+ delete stripped.receiverType;
1163
+ delete stripped.receiverTypeSource;
1164
+ delete stripped.receiverTypeEvidence;
1165
+ delete stripped.receiverTypeQualifier;
1166
+ return stripped;
1167
+ }
1168
+
1169
+ module.exports = {
1170
+ externalTypeDenotation,
1171
+ externalReceiverVerdict,
1172
+ externalProducerVia,
1173
+ externalTypeGate,
1174
+ stripExternalReceiverType,
1175
+ siteWrittenType,
1176
+ bindingIsExternal,
1177
+ cargoDependencies,
1178
+ csharpGlobalUsings,
1179
+ csharpUsings,
1180
+ csharpResolveUsingName,
1181
+ csharpDeclaredNames,
1182
+ rustBindingsInScope,
1183
+ siteNamespaceOf,
1184
+ projectNamespaces,
1185
+ };