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