ucn 5.4.2 → 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.
- package/.claude/skills/ucn/SKILL.md +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +18 -12
- package/cli/index.js +40 -14
- package/core/accessors.js +118 -3
- package/core/account.js +30 -0
- package/core/analysis.js +508 -264
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +522 -31
- package/core/callers.js +12053 -1047
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +7 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +305 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +457 -60
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +2113 -0
- package/core/rust-modules.js +185 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1161 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2237 -115
- package/languages/utils.js +298 -4
- package/mcp/server.js +22 -8
- package/package.json +1 -1
|
@@ -0,0 +1,1161 @@
|
|
|
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
|
+
const declares = (index.symbols.get(name) || []).some(definition => definition.file === filePath &&
|
|
778
|
+
(TYPE_KINDS.has(definition.type) || definition.type === 'type'));
|
|
779
|
+
const imports = (fileEntry.importBindings || []).some(binding =>
|
|
780
|
+
binding.name !== '*' && (binding.alias || binding.name) === name && binding.module);
|
|
781
|
+
return declares || imports ? line : null;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** Bindings visible at `line`: those of the innermost enclosing scope that
|
|
785
|
+
* declares any (a scope's `use` shadows its parents'); none when the line is
|
|
786
|
+
* unknown and the bindings disagree in scope. */
|
|
787
|
+
function rustBindingsInScope(fileEntry, bindings, line) {
|
|
788
|
+
const scopes = rustScopes(fileEntry);
|
|
789
|
+
const scopeOf = binding => binding.line == null ? null : innermostScope(scopes, binding.line);
|
|
790
|
+
if (line == null) {
|
|
791
|
+
return new Set(bindings.map(scopeOf)).size === 1 ? bindings : [];
|
|
792
|
+
}
|
|
793
|
+
let best = null;
|
|
794
|
+
let visible = [];
|
|
795
|
+
for (const binding of bindings) {
|
|
796
|
+
const scope = scopeOf(binding);
|
|
797
|
+
if (scope && !(scope.startLine <= line && scope.endLine >= line)) continue;
|
|
798
|
+
const span = scope ? scope.endLine - scope.startLine : Infinity;
|
|
799
|
+
if (best === null || span < best) { best = span; visible = [binding]; }
|
|
800
|
+
else if (span === best) visible.push(binding);
|
|
801
|
+
}
|
|
802
|
+
return visible;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function qualifiedDenotation(index, fileEntry, filePath, name, qualifier) {
|
|
806
|
+
const language = fileEntry.language;
|
|
807
|
+
switch (language) {
|
|
808
|
+
case 'rust':
|
|
809
|
+
return rustPathIsExternal(index, fileEntry, filePath, qualifier, null)
|
|
810
|
+
? { original: name, via: `${qualifier}::${name}` } : null;
|
|
811
|
+
case 'cpp': {
|
|
812
|
+
const root = String(qualifier).replace(/^::/, '').split('::')[0];
|
|
813
|
+
if (!langTraits(language)?.standardPathRoots?.includes(root)) return null;
|
|
814
|
+
// A project that declares the namespace itself (`namespace std`
|
|
815
|
+
// specializations) keeps its own resolution.
|
|
816
|
+
const declares = (index.symbols.get(name) || []).some(definition =>
|
|
817
|
+
String(definition.namespace || '').split('::')[0] === root);
|
|
818
|
+
return declares ? null : { original: name, via: `${qualifier}::${name}` };
|
|
819
|
+
}
|
|
820
|
+
case 'java': {
|
|
821
|
+
if (!/^[a-z_]/.test(qualifier)) return null;
|
|
822
|
+
const bindingIsType = (fileEntry.importBindings || []).some(binding =>
|
|
823
|
+
(binding.alias || binding.name) === qualifier.split('.')[0]);
|
|
824
|
+
if (bindingIsType) return null;
|
|
825
|
+
const verdict = bindingIsExternal(index, fileEntry, filePath, { module: `${qualifier}.${name}`, name });
|
|
826
|
+
if (!verdict) return null;
|
|
827
|
+
return { original: name, via: `${qualifier}.${name}`, ...(verdict !== true && { certain: false }) };
|
|
828
|
+
}
|
|
829
|
+
case 'javascript': case 'typescript': case 'tsx': case 'python': {
|
|
830
|
+
const bindings = (fileEntry.importBindings || []).filter(binding =>
|
|
831
|
+
(binding.alias || binding.name) === qualifier && binding.module);
|
|
832
|
+
if (bindings.length === 0) return null;
|
|
833
|
+
if (!bindings.every(binding =>
|
|
834
|
+
['import', 'namespace', 'require', 'default'].includes(binding.kind))) return null;
|
|
835
|
+
return bindings.every(binding => bindingIsExternal(index, fileEntry, filePath, binding))
|
|
836
|
+
? { original: name, via: `${bindings[0].module}.${name}` } : null;
|
|
837
|
+
}
|
|
838
|
+
default:
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* The type a call record names in writing, or null.
|
|
845
|
+
* { name, qualifier, receiverTyped } - receiverTyped marks a receiver whose
|
|
846
|
+
* TYPE (not the callee name) was written: annotation or constructor.
|
|
847
|
+
*/
|
|
848
|
+
function siteWrittenType(index, fileEntry, call) {
|
|
849
|
+
const written = siteWrittenTypeRaw(index, fileEntry, call);
|
|
850
|
+
if (!written || written.qualifier || !langTraits(fileEntry.language)?.typeNameScoping ||
|
|
851
|
+
!projectTypeNamed(index, written.name)) return written;
|
|
852
|
+
// Java/C# (fix #372): an unqualified name is resolved by package or
|
|
853
|
+
// namespace scope, so the site's own spelling must be unqualified. The
|
|
854
|
+
// parser keeps the last segment of `new b.Thread()` / `b.Thread t`; the
|
|
855
|
+
// written text at the recorded span decides.
|
|
856
|
+
const span = written.receiverTyped
|
|
857
|
+
? call.receiverTypeEvidence
|
|
858
|
+
: (call.isMethod ? null : call.callSite);
|
|
859
|
+
if (written.receiverTyped || !call.isMethod) {
|
|
860
|
+
const spelled = spelledTypeQualifier(index, fileEntry, span, written.name);
|
|
861
|
+
if (spelled === undefined) return { ...written, unscoped: true };
|
|
862
|
+
if (spelled) return { ...written, qualifier: spelled };
|
|
863
|
+
}
|
|
864
|
+
return written;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/** The qualifier a type name is spelled with at a recorded source span
|
|
868
|
+
* (`new b.Thread()` -> 'b', `Thread` -> null); undefined when unknown. */
|
|
869
|
+
function spelledTypeQualifier(index, fileEntry, span, name) {
|
|
870
|
+
if (!span || !Number.isInteger(span.start) || !Number.isInteger(span.end)) return undefined;
|
|
871
|
+
const filePath = path.join(index.root, fileEntry.relativePath);
|
|
872
|
+
let content;
|
|
873
|
+
try { content = index._readFile(filePath); } catch { return undefined; }
|
|
874
|
+
if (typeof content !== 'string') return undefined;
|
|
875
|
+
const text = content.slice(span.start, span.end).replace(/^\s*new\b/, '')
|
|
876
|
+
.replace(/[<([{].*$/s, '').replace(/\s+/g, '');
|
|
877
|
+
if (text === name) return null;
|
|
878
|
+
if (!text.endsWith(`.${name}`)) return undefined;
|
|
879
|
+
return text.slice(0, -name.length - 1).replace(/^global::/, '') || undefined;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function siteWrittenTypeRaw(index, fileEntry, call) {
|
|
883
|
+
const language = fileEntry.language;
|
|
884
|
+
if (!call.isMethod) {
|
|
885
|
+
// Constructor-shaped call: the callee name is a type name.
|
|
886
|
+
if (!call.isConstructor && !(index.symbols.get(call.name) || []).some(definition =>
|
|
887
|
+
TYPE_KINDS.has(definition.type))) return null;
|
|
888
|
+
return { name: call.name, qualifier: (call.isConstructor && call.receiver) || null, receiverTyped: false };
|
|
889
|
+
}
|
|
890
|
+
if (language === 'rust' && call.isPathCall && call.receiver && !call.receiverType) {
|
|
891
|
+
const segments = String(call.receiver).split('::').filter(Boolean);
|
|
892
|
+
const last = segments[segments.length - 1];
|
|
893
|
+
if (!last || !/^[A-Z]/.test(last) || last === 'Self') return null;
|
|
894
|
+
return { name: last, qualifier: segments.length > 1 ? segments.slice(0, -1).join('::') : null,
|
|
895
|
+
receiverTyped: false };
|
|
896
|
+
}
|
|
897
|
+
if (call.receiverIsTypeQualified && call.receiver && !call.receiverType &&
|
|
898
|
+
(language === 'java' || language === 'csharp')) {
|
|
899
|
+
return { name: call.receiver, qualifier: call.receiverTypeQualifier || null, receiverTyped: false };
|
|
900
|
+
}
|
|
901
|
+
if (call.receiverType && (call.receiverTypeSource === 'annotation' ||
|
|
902
|
+
call.receiverTypeSource === 'constructor')) {
|
|
903
|
+
// A C++ declaration's written namespace qualifies the declared type
|
|
904
|
+
// itself, not a pointee (`std::unique_ptr<file> p; p->f()`).
|
|
905
|
+
const namespace = call.receiverTypeNamespace &&
|
|
906
|
+
call.receiverTypeEvidence?.type === call.receiverType ? call.receiverTypeNamespace : null;
|
|
907
|
+
return { name: call.receiverType,
|
|
908
|
+
qualifier: call.receiverTypeQualifier || namespace || null, receiverTyped: true,
|
|
909
|
+
source: call.receiverTypeSource };
|
|
910
|
+
}
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function ownerName(definition) {
|
|
915
|
+
if (TYPE_KINDS.has(definition.type)) return definition.name;
|
|
916
|
+
return definition.className || String(definition.receiver || '').replace(/^[*&]\s*/, '') || null;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* What a target definition's owner type denotes: 'project' (a project type
|
|
921
|
+
* definition), 'external' (an impl/extension on the external type), or
|
|
922
|
+
* 'unknown'.
|
|
923
|
+
*/
|
|
924
|
+
function ownerDenotation(index, definition, original) {
|
|
925
|
+
const key = `\x01owner\x00${definition.file}:${definition.startLine}:${definition.name}:${original}`;
|
|
926
|
+
return memoized(index, key, () => {
|
|
927
|
+
if (TYPE_KINDS.has(definition.type)) return 'project';
|
|
928
|
+
const owner = ownerName(definition);
|
|
929
|
+
if (definition.implSelfQualifier) {
|
|
930
|
+
return externalTypeDenotation(index, definition.file, owner, definition.implSelfQualifier, definition.startLine)
|
|
931
|
+
? 'external' : 'project';
|
|
932
|
+
}
|
|
933
|
+
// A project type that declares the external type as an ancestor
|
|
934
|
+
// (`class File extends java.io.File`) receives its dispatch.
|
|
935
|
+
const ownerDefs = (index.symbols.get(owner) || []).filter(candidate =>
|
|
936
|
+
TYPE_KINDS.has(candidate.type));
|
|
937
|
+
if (ownerDefs.some(candidate => (index._getInheritanceParents?.(owner, candidate.file) || [])
|
|
938
|
+
.some(parent => String(parent).split(/::|\./).pop() === original))) return 'external';
|
|
939
|
+
if (ownerDefs.some(candidate => candidate.file === definition.file)) return 'project';
|
|
940
|
+
if (externalTypeDenotation(index, definition.file, owner, null, definition.startLine)) return 'external';
|
|
941
|
+
return ownerDefs.length > 0 ? 'project' : 'unknown';
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function projectTypeNamed(index, name) {
|
|
946
|
+
return (index.symbols.get(name) || []).some(definition => TYPE_KINDS.has(definition.type));
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Verdict for a call site against candidate target definitions (the pinned
|
|
951
|
+
* targets on the caller side, every same-name callable on the callee side).
|
|
952
|
+
* Returns null (the gate does not apply) or one of:
|
|
953
|
+
* { verdict: 'exclude', reason: 'external-receiver', via }
|
|
954
|
+
* { verdict: 'dispatch', via } an impl on the external type may supply it
|
|
955
|
+
* { verdict: 'strip', via } receiver typed by the external name: the
|
|
956
|
+
* engine resolves it without that type
|
|
957
|
+
*/
|
|
958
|
+
function externalTypeGate(index, filePath, fileEntry, call, targetsOrThunk) {
|
|
959
|
+
if (!fileEntry || !targetsOrThunk) return null;
|
|
960
|
+
// Go: a receiver typed by an external package's type may be an interface
|
|
961
|
+
// every project type with the method set satisfies; the existing
|
|
962
|
+
// package-qualified physics routes it (possible dispatch).
|
|
963
|
+
if (fileEntry.language === 'go') return null;
|
|
964
|
+
const written = siteWrittenType(index, fileEntry, call);
|
|
965
|
+
if (!written) return null;
|
|
966
|
+
const denotation = externalTypeDenotation(index, filePath, written.name, written.qualifier, call.line,
|
|
967
|
+
{ unscoped: written.unscoped === true });
|
|
968
|
+
if (!denotation) return null;
|
|
969
|
+
const { original, via } = denotation;
|
|
970
|
+
// Only a real collision needs the gate: without a project type of that
|
|
971
|
+
// name the engine's own resolution cannot conflate the two.
|
|
972
|
+
if (!projectTypeNamed(index, original)) return null;
|
|
973
|
+
const targets = typeof targetsOrThunk === 'function' ? targetsOrThunk() : targetsOrThunk;
|
|
974
|
+
if (!targets || targets.length === 0) return null;
|
|
975
|
+
const collision = [];
|
|
976
|
+
const others = [];
|
|
977
|
+
for (const target of targets) (ownerName(target) === original ? collision : others).push(target);
|
|
978
|
+
// An unresolvable non-standard package: the name is not the project
|
|
979
|
+
// type's, but a resolver gap cannot be ruled out - never confirmed,
|
|
980
|
+
// never excluded.
|
|
981
|
+
if (denotation.certain === false) return collision.length > 0 ? { verdict: 'unverified', via } : null;
|
|
982
|
+
// A target the same-name PROJECT type reaches (its ancestor, a trait it
|
|
983
|
+
// implements, a blanket impl) is where the engine could conflate the two
|
|
984
|
+
// types; the external type may reach it too, so it stays visible.
|
|
985
|
+
const related = written.receiverTyped
|
|
986
|
+
? others.filter(target => reachableThroughNamesake(index, target, original)) : [];
|
|
987
|
+
if (collision.length === 0) return related.length > 0 ? { verdict: 'dispatch', via } : null;
|
|
988
|
+
let external = 0;
|
|
989
|
+
for (const target of collision) {
|
|
990
|
+
const verdict = ownerDenotation(index, target, original);
|
|
991
|
+
if (verdict === 'external') external++;
|
|
992
|
+
else if (verdict !== 'project') return written.receiverTyped ? { verdict: 'strip', via } : null;
|
|
993
|
+
}
|
|
994
|
+
if (external > 0) return { verdict: 'dispatch', via };
|
|
995
|
+
// Structural typing: an annotated external type admits any value of the
|
|
996
|
+
// right shape, so only a constructed receiver is exact.
|
|
997
|
+
if (written.receiverTyped && written.source !== 'constructor' &&
|
|
998
|
+
langTraits(fileEntry.language)?.typeSystem === 'structural') return { verdict: 'dispatch', via };
|
|
999
|
+
return related.length > 0
|
|
1000
|
+
? { verdict: 'strip', via }
|
|
1001
|
+
: { verdict: 'exclude', reason: 'external-receiver', via };
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Verdict for a receiver whose declared type (a field, written in its own
|
|
1006
|
+
* file) denotes the external type `original`: 'exclude' when no target can
|
|
1007
|
+
* be reached by a value of that type, 'dispatch' when an impl on the
|
|
1008
|
+
* external type, a blanket impl, an extension method or a trait it may
|
|
1009
|
+
* implement could supply one, 'unknown' otherwise. Structural typing admits
|
|
1010
|
+
* any value of the right shape: always 'dispatch'.
|
|
1011
|
+
*/
|
|
1012
|
+
function externalReceiverVerdict(index, language, targets, original) {
|
|
1013
|
+
if (!targets || targets.length === 0) return 'unknown';
|
|
1014
|
+
if (langTraits(language)?.typeSystem !== 'nominal' || language === 'go') return 'dispatch';
|
|
1015
|
+
let dispatch = false;
|
|
1016
|
+
for (const target of targets) {
|
|
1017
|
+
const owner = ownerName(target);
|
|
1018
|
+
if (!owner) continue;
|
|
1019
|
+
if (owner === original) {
|
|
1020
|
+
const verdict = ownerDenotation(index, target, original);
|
|
1021
|
+
if (verdict === 'external') dispatch = true;
|
|
1022
|
+
else if (verdict !== 'project') return 'unknown';
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (target.isExtensionMethod || reachableByExternal(index, target, original)) dispatch = true;
|
|
1026
|
+
}
|
|
1027
|
+
return dispatch ? 'dispatch' : 'exclude';
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Can a value of the external type `name` reach `target`? A blanket impl
|
|
1032
|
+
* member; a trait member that some impl for `name`, or a blanket impl,
|
|
1033
|
+
* provides; or a member of a project type that implements / extends the
|
|
1034
|
+
* external type itself (`class MyList implements java.util.List`, `impl
|
|
1035
|
+
* std::io::Write for MyWriter`) - the value may be such an instance.
|
|
1036
|
+
*/
|
|
1037
|
+
function reachableByExternal(index, target, name) {
|
|
1038
|
+
const owner = ownerName(target);
|
|
1039
|
+
if (target.ownerGenerics && genericParamNames(target.ownerGenerics).has(owner)) return true;
|
|
1040
|
+
if (target.traitName === name) return true;
|
|
1041
|
+
const key = `\x01extreach\x00${target.file}:${target.startLine}:${target.name}\x00${name}`;
|
|
1042
|
+
return memoized(index, key, () => {
|
|
1043
|
+
const sameName = index.symbols.get(target.name) || [];
|
|
1044
|
+
if (sameName.some(candidate => candidate.traitName === owner && (candidate.className === name ||
|
|
1045
|
+
(candidate.ownerGenerics && genericParamNames(candidate.ownerGenerics).has(candidate.className))))) {
|
|
1046
|
+
return true;
|
|
1047
|
+
}
|
|
1048
|
+
const seen = new Set([owner]);
|
|
1049
|
+
let level = (index.symbols.get(owner) || []).filter(d => TYPE_KINDS.has(d.type));
|
|
1050
|
+
for (let depth = 0; depth < 8 && level.length > 0; depth++) {
|
|
1051
|
+
const next = [];
|
|
1052
|
+
for (const definition of level) {
|
|
1053
|
+
for (const parent of index._getInheritanceParents?.(definition.name, definition.file) || []) {
|
|
1054
|
+
const parentName = String(parent).replace(/<.*$/s, '').split(/::|\./).pop();
|
|
1055
|
+
if (parentName === name) return true;
|
|
1056
|
+
if (seen.has(parentName)) continue;
|
|
1057
|
+
seen.add(parentName);
|
|
1058
|
+
next.push(...(index.symbols.get(parentName) || []).filter(d => TYPE_KINDS.has(d.type)));
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
level = next;
|
|
1062
|
+
}
|
|
1063
|
+
return false;
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function genericParamNames(text) {
|
|
1068
|
+
const inner = String(text || '').trim().replace(/^</, '').replace(/>$/, '');
|
|
1069
|
+
const names = new Set();
|
|
1070
|
+
let depth = 0;
|
|
1071
|
+
let start = 0;
|
|
1072
|
+
const parts = [];
|
|
1073
|
+
for (let i = 0; i < inner.length; i++) {
|
|
1074
|
+
const ch = inner[i];
|
|
1075
|
+
if (ch === '<' || ch === '(' || ch === '[') depth++;
|
|
1076
|
+
else if (ch === '>' || ch === ')' || ch === ']') depth--;
|
|
1077
|
+
else if (ch === ',' && depth === 0) { parts.push(inner.slice(start, i)); start = i + 1; }
|
|
1078
|
+
}
|
|
1079
|
+
parts.push(inner.slice(start));
|
|
1080
|
+
for (const part of parts) {
|
|
1081
|
+
const match = part.trim().match(/^([A-Za-z_][A-Za-z0-9_]*)/);
|
|
1082
|
+
if (match && !part.trim().startsWith("'")) names.add(match[1]);
|
|
1083
|
+
}
|
|
1084
|
+
return names;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Could a value of the project type named `name` reach `target` other than
|
|
1089
|
+
* as its owner: a blanket impl member (generic owner), a trait/interface the
|
|
1090
|
+
* type implements, or an ancestor class? Name-level, bounded.
|
|
1091
|
+
*/
|
|
1092
|
+
function reachableThroughNamesake(index, target, name) {
|
|
1093
|
+
const owner = ownerName(target);
|
|
1094
|
+
if (!owner) return false;
|
|
1095
|
+
const key = `\x01reach\x00${target.file}:${target.startLine}:${target.name}\x00${name}`;
|
|
1096
|
+
return memoized(index, key, () => {
|
|
1097
|
+
if (target.ownerGenerics && genericParamNames(target.ownerGenerics).has(owner)) return true;
|
|
1098
|
+
if ((index.symbols.get(target.name) || []).some(candidate =>
|
|
1099
|
+
candidate.className === name && candidate.traitName === owner)) return true;
|
|
1100
|
+
const seen = new Set([name]);
|
|
1101
|
+
let level = (index.symbols.get(name) || []).filter(d => TYPE_KINDS.has(d.type));
|
|
1102
|
+
for (let depth = 0; depth < 8 && level.length > 0; depth++) {
|
|
1103
|
+
const next = [];
|
|
1104
|
+
for (const definition of level) {
|
|
1105
|
+
for (const parent of index._getInheritanceParents?.(definition.name, definition.file) || []) {
|
|
1106
|
+
const parentName = String(parent).replace(/<.*$/s, '').split(/::|\./).pop();
|
|
1107
|
+
if (parentName === owner) return true;
|
|
1108
|
+
if (seen.has(parentName)) continue;
|
|
1109
|
+
seen.add(parentName);
|
|
1110
|
+
next.push(...(index.symbols.get(parentName) || []).filter(d => TYPE_KINDS.has(d.type)));
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
level = next;
|
|
1114
|
+
}
|
|
1115
|
+
return false;
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* A producer call whose written type (type-qualified path / static factory /
|
|
1121
|
+
* constructor) denotes an external type that shares its name with a project
|
|
1122
|
+
* type: the external evidence string, else null. The value it produces was
|
|
1123
|
+
* decided outside the project.
|
|
1124
|
+
*/
|
|
1125
|
+
function externalProducerVia(index, filePath, fileEntry, call) {
|
|
1126
|
+
if (!fileEntry) return null;
|
|
1127
|
+
const written = siteWrittenType(index, fileEntry, call);
|
|
1128
|
+
if (!written || written.receiverTyped) return null;
|
|
1129
|
+
const denotation = externalTypeDenotation(index, filePath, written.name, written.qualifier, call.line,
|
|
1130
|
+
{ unscoped: written.unscoped === true });
|
|
1131
|
+
if (!denotation || denotation.certain === false || !projectTypeNamed(index, denotation.original)) return null;
|
|
1132
|
+
return `${denotation.via}${fileEntry.language === 'rust' || fileEntry.language === 'cpp' ? '::' : '.'}${call.name}`;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/** A copy of the call whose external-typed receiver no longer names a type. */
|
|
1136
|
+
function stripExternalReceiverType(call, via) {
|
|
1137
|
+
const stripped = { ...call, receiverExternalFlow: via };
|
|
1138
|
+
delete stripped.receiverType;
|
|
1139
|
+
delete stripped.receiverTypeSource;
|
|
1140
|
+
delete stripped.receiverTypeEvidence;
|
|
1141
|
+
delete stripped.receiverTypeQualifier;
|
|
1142
|
+
return stripped;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
module.exports = {
|
|
1146
|
+
externalTypeDenotation,
|
|
1147
|
+
externalReceiverVerdict,
|
|
1148
|
+
externalProducerVia,
|
|
1149
|
+
externalTypeGate,
|
|
1150
|
+
stripExternalReceiverType,
|
|
1151
|
+
siteWrittenType,
|
|
1152
|
+
bindingIsExternal,
|
|
1153
|
+
cargoDependencies,
|
|
1154
|
+
csharpGlobalUsings,
|
|
1155
|
+
csharpUsings,
|
|
1156
|
+
csharpResolveUsingName,
|
|
1157
|
+
csharpDeclaredNames,
|
|
1158
|
+
rustBindingsInScope,
|
|
1159
|
+
siteNamespaceOf,
|
|
1160
|
+
projectNamespaces,
|
|
1161
|
+
};
|