ucn 5.5.2 → 5.6.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 +35 -12
- package/.claude/skills/ucn/references/commands.md +21 -17
- package/.claude/skills/ucn/references/resolution-rules.md +4 -0
- package/.claude/skills/ucn/references/trust-contract.md +4 -4
- package/CHANGELOG.md +474 -0
- package/README.md +71 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +138 -324
- package/core/account.js +44 -87
- package/core/analysis.js +273 -107
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +29 -65
- package/core/cache.js +101 -16
- package/core/callers.js +352 -70
- package/core/capture.js +331 -0
- package/core/change-kinds.js +888 -0
- package/core/check.js +1117 -139
- package/core/client-receivers.js +189 -0
- package/core/command-contracts.js +7 -6
- package/core/constructed-class.js +1195 -0
- package/core/contract-membership.js +142 -0
- package/core/deadcode.js +388 -85
- package/core/deletion.js +401 -0
- package/core/discovery.js +104 -9
- package/core/entrypoints.js +12 -6
- package/core/evidence-bands.js +68 -0
- package/core/evidence-bands.json +44 -0
- package/core/execute.js +172 -161
- package/core/hot-summary.js +241 -0
- package/core/index-ir.js +3 -1
- package/core/inprocess-clients.js +41 -15
- package/core/ir.js +5 -1
- package/core/member-signatures.js +789 -0
- package/core/output/analysis-ext.js +20 -10
- package/core/output/analysis.js +98 -60
- package/core/output/check.js +98 -37
- package/core/output/doctor.js +34 -1
- package/core/output/find.js +2 -1
- package/core/output/lines.js +63 -3
- package/core/output/public.js +30 -11
- package/core/output/refactoring.js +32 -0
- package/core/output/reporting.js +21 -2
- package/core/output/search.js +11 -2
- package/core/output/tracing.js +35 -7
- package/core/output-budget.js +124 -57
- package/core/parallel-build.js +3 -3
- package/core/project.js +106 -23
- package/core/provenance-type.js +12 -5
- package/core/public-command.js +35 -9
- package/core/python-modules.js +6 -0
- package/core/reflection.js +166 -23
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/request-helpers.js +63 -7
- package/core/review-items.js +75 -0
- package/core/search.js +89 -128
- package/core/shared.js +0 -45
- package/core/snapshot.js +350 -0
- package/core/source-universe.js +707 -0
- package/core/test-units.js +3624 -0
- package/core/tracing.js +150 -96
- package/core/verify.js +27 -11
- package/languages/adapter.js +8 -3
- package/languages/c-family.js +12 -0
- package/languages/csharp.js +634 -0
- package/languages/go.js +120 -0
- package/languages/index.js +422 -4
- package/languages/java.js +712 -0
- package/languages/javascript.js +1460 -2
- package/languages/lexical-scope.js +68 -0
- package/languages/python.js +1672 -1
- package/languages/rust.js +91 -0
- package/mcp/server.js +44 -110
- package/package.json +11 -6
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/member-signatures.js - Is a method a deriving class declares the
|
|
3
|
+
* method it inherits under that name, and which class does a written base
|
|
4
|
+
* name denote?
|
|
5
|
+
*
|
|
6
|
+
* Where a language overloads, a method is the inherited one only when its
|
|
7
|
+
* parameters have the same types, and a type is what its name denotes
|
|
8
|
+
* where it is written, never how it is spelled: `int` and `System.Int32`
|
|
9
|
+
* are one type, `int` and `int?` two, `Left.Token` and `Right.Token` two,
|
|
10
|
+
* `Token` whichever of them the file's imports say.
|
|
11
|
+
*
|
|
12
|
+
* The readers give each parameter's type as it is written in the syntax
|
|
13
|
+
* tree (`params`). This module resolves the names by the language's own
|
|
14
|
+
* lookup (`typeNameScoping`): the method's and the classes' type
|
|
15
|
+
* parameters, the member types of the classes around the site (their own
|
|
16
|
+
* and the ones they inherit), then
|
|
17
|
+
* - 'package-on-demand' (JLS 6.4.1): the file's own types and
|
|
18
|
+
* single-type imports, the package, on-demand imports with the
|
|
19
|
+
* implicit one;
|
|
20
|
+
* - 'namespace-usings' (C# spec, namespace and type names): for each
|
|
21
|
+
* enclosing namespace from the innermost out, its members, its using
|
|
22
|
+
* aliases, the types its using directives bring; global usings and
|
|
23
|
+
* project-file <Using> items at the outermost.
|
|
24
|
+
*
|
|
25
|
+
* The answer has three values. 'same' and 'different' are proofs over
|
|
26
|
+
* what the project's source declares and the file imports. 'unknown' is
|
|
27
|
+
* everything else: a type parameter of a class (its argument is written
|
|
28
|
+
* in a base list this does not substitute), a name only a package outside
|
|
29
|
+
* the project can declare, a name an unread base class may declare inside
|
|
30
|
+
* itself, an alias this does not read. Nothing is decided by spelling
|
|
31
|
+
* except where spelling is proof: two names that are no alias and differ
|
|
32
|
+
* in their last part are two types, and one spelling read in one scope is
|
|
33
|
+
* one type.
|
|
34
|
+
*
|
|
35
|
+
* Assumed, and said in the documentation: the code compiles (a name has
|
|
36
|
+
* one meaning where it is used), and the packages and namespaces the
|
|
37
|
+
* project declares types in, and the namespaces around those, hold the
|
|
38
|
+
* types its source declares.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
'use strict';
|
|
42
|
+
|
|
43
|
+
const { langTraits } = require('../languages');
|
|
44
|
+
const { csharpUsings, csharpDeclaredNames, projectNamespaces, siteNamespaceOf } = require('./type-denotation');
|
|
45
|
+
|
|
46
|
+
const TYPE_KINDS = new Set(['class', 'struct', 'interface', 'enum', 'record', 'delegate']);
|
|
47
|
+
const VALUE_KINDS = new Set(['struct', 'enum']);
|
|
48
|
+
const REFERENCE_KINDS = new Set(['class', 'interface', 'delegate']);
|
|
49
|
+
|
|
50
|
+
const SAME = 'same';
|
|
51
|
+
const DIFFERENT = 'different';
|
|
52
|
+
const UNKNOWN = 'unknown';
|
|
53
|
+
|
|
54
|
+
/** Several answers that must all hold: one 'different' decides, else one 'unknown'. */
|
|
55
|
+
function every(answers) {
|
|
56
|
+
if (answers.includes(DIFFERENT)) return DIFFERENT;
|
|
57
|
+
return answers.includes(UNKNOWN) ? UNKNOWN : SAME;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const lastOf = dotted => String(dotted).split('.').pop();
|
|
61
|
+
|
|
62
|
+
function kindOf(term) {
|
|
63
|
+
if (term.open) return 'open';
|
|
64
|
+
if (term.array) return 'array';
|
|
65
|
+
if (term.nullable) return 'nullable';
|
|
66
|
+
if (term.tuple) return 'tuple';
|
|
67
|
+
if (term.pointer) return 'pointer';
|
|
68
|
+
if (term.generic) return 'generic';
|
|
69
|
+
if (term.variable != null) return 'variable';
|
|
70
|
+
return 'name';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Is every entry of one list in the other? */
|
|
74
|
+
const within = (a, b) => a.every(value => b.includes(value));
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Do two unresolved names stand in one scope: the same package or
|
|
78
|
+
* namespace, the same bindings for the name, and on-demand imports (using
|
|
79
|
+
* directives) of packages outside the project of which one file's are
|
|
80
|
+
* all among the other's. A name that compiles has one provider among
|
|
81
|
+
* them; the file with more sees the same one. (A package the project
|
|
82
|
+
* declares types in holds none of that name, or the name was resolved.)
|
|
83
|
+
*/
|
|
84
|
+
function sameScope(a, b) {
|
|
85
|
+
return !!a && !!b && a.base === b.base && (within(a.usings, b.usings) || within(b.usings, a.usings));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Two resolved names. */
|
|
89
|
+
function compareNames(a, b) {
|
|
90
|
+
// A name an unread base class may declare inside itself: what was
|
|
91
|
+
// found past that class holds only if the class declares none, and the
|
|
92
|
+
// same spelling past the same classes is one type either way.
|
|
93
|
+
if (a.unless || b.unless) {
|
|
94
|
+
if (a.last !== b.last) return DIFFERENT;
|
|
95
|
+
return a.unless === b.unless && a.text === b.text && a.fq === b.fq ? SAME : UNKNOWN;
|
|
96
|
+
}
|
|
97
|
+
if (a.fq === b.fq) return SAME;
|
|
98
|
+
// A member type can be written through a class that inherits it: two
|
|
99
|
+
// qualified spellings that are not canonical may be one type.
|
|
100
|
+
if ((a.canonical === false || b.canonical === false) && a.last === b.last) return UNKNOWN;
|
|
101
|
+
return DIFFERENT;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** One side is a name that was not resolved. */
|
|
105
|
+
function compareOpen(a, b) {
|
|
106
|
+
if (a.open && b.open) {
|
|
107
|
+
if (a.named && b.named && a.last !== b.last) return DIFFERENT;
|
|
108
|
+
return a.text === b.text && sameScope(a.scope, b.scope) ? SAME : UNKNOWN;
|
|
109
|
+
}
|
|
110
|
+
const [open, other] = a.open ? [a, b] : [b, a];
|
|
111
|
+
// A type parameter of a class, an alias that is not read: any type.
|
|
112
|
+
if (!open.named) return UNKNOWN;
|
|
113
|
+
switch (kindOf(other)) {
|
|
114
|
+
case 'name':
|
|
115
|
+
if (open.last !== other.last) return DIFFERENT;
|
|
116
|
+
// A name the file's on-demand imports (using directives) bring
|
|
117
|
+
// and nothing nearer declares: the type of that name in one of
|
|
118
|
+
// the imported packages is it, for a second provider would
|
|
119
|
+
// leave the name ambiguous. Both files compile together.
|
|
120
|
+
if (open.simple && open.scope && !open.scope.guarded && !other.project && !other.unless &&
|
|
121
|
+
open.scope.unit != null && open.scope.unit === other.unit &&
|
|
122
|
+
open.scope.providers.some(provider => `${provider}.${open.last}` === other.fq)) return SAME;
|
|
123
|
+
return UNKNOWN;
|
|
124
|
+
case 'nullable':
|
|
125
|
+
// `T?` of a type of unknown kind may be T itself.
|
|
126
|
+
if (other.maybe) return compareTypes(open, other.nullable) === DIFFERENT ? DIFFERENT : UNKNOWN;
|
|
127
|
+
return DIFFERENT;
|
|
128
|
+
case 'generic':
|
|
129
|
+
// A name written without type arguments is no type that takes
|
|
130
|
+
// some; one that takes none may be the type inside generic
|
|
131
|
+
// types that this one is.
|
|
132
|
+
return open.last !== other.generic.last ? DIFFERENT : UNKNOWN;
|
|
133
|
+
default:
|
|
134
|
+
// A declared type is no array, tuple, pointer or type parameter.
|
|
135
|
+
return DIFFERENT;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Two resolved parameter types: 'same', 'different' or 'unknown'.
|
|
141
|
+
*/
|
|
142
|
+
function compareTypes(a, b) {
|
|
143
|
+
if (!a || !b) return UNKNOWN;
|
|
144
|
+
if (a.open || b.open) return compareOpen(a, b);
|
|
145
|
+
const ka = kindOf(a);
|
|
146
|
+
const kb = kindOf(b);
|
|
147
|
+
if (ka !== kb) {
|
|
148
|
+
// `T?` of a type of unknown kind may be T itself.
|
|
149
|
+
if (ka === 'nullable' && a.maybe) return compareTypes(a.nullable, b) === DIFFERENT ? DIFFERENT : UNKNOWN;
|
|
150
|
+
if (kb === 'nullable' && b.maybe) return compareTypes(a, b.nullable) === DIFFERENT ? DIFFERENT : UNKNOWN;
|
|
151
|
+
// A nullable value or a tuple is a generic type of the platform
|
|
152
|
+
// written another way: a generic type the project does not
|
|
153
|
+
// declare may be it.
|
|
154
|
+
const generic = ka === 'generic' ? a : kb === 'generic' ? b : null;
|
|
155
|
+
const written = generic === a ? kb : ka;
|
|
156
|
+
if (generic && (written === 'nullable' || written === 'tuple') &&
|
|
157
|
+
(generic.generic.open || generic.generic.project !== true)) return UNKNOWN;
|
|
158
|
+
return DIFFERENT;
|
|
159
|
+
}
|
|
160
|
+
switch (ka) {
|
|
161
|
+
case 'name': return compareNames(a, b);
|
|
162
|
+
case 'variable': return a.variable === b.variable ? SAME : DIFFERENT;
|
|
163
|
+
case 'array': return a.rank !== b.rank ? DIFFERENT : compareTypes(a.array, b.array);
|
|
164
|
+
case 'nullable': return compareTypes(a.nullable, b.nullable);
|
|
165
|
+
case 'pointer': return compareTypes(a.pointer, b.pointer);
|
|
166
|
+
case 'tuple':
|
|
167
|
+
if (a.tuple.length !== b.tuple.length) return DIFFERENT;
|
|
168
|
+
return every(a.tuple.map((each, i) => compareTypes(each, b.tuple[i])));
|
|
169
|
+
case 'generic': {
|
|
170
|
+
const head = compareTypes(a.generic, b.generic);
|
|
171
|
+
if (head === DIFFERENT) return DIFFERENT;
|
|
172
|
+
// The type's own arguments, then those of the types around it.
|
|
173
|
+
const ownA = a.args.slice(a.args.length - a.own);
|
|
174
|
+
const ownB = b.args.slice(b.args.length - b.own);
|
|
175
|
+
if (ownA.length !== ownB.length) return DIFFERENT;
|
|
176
|
+
const own = every(ownA.map((each, i) => compareTypes(each, ownB[i])));
|
|
177
|
+
if (own === DIFFERENT) return DIFFERENT;
|
|
178
|
+
const aroundA = a.args.slice(0, a.args.length - a.own);
|
|
179
|
+
const aroundB = b.args.slice(0, b.args.length - b.own);
|
|
180
|
+
// Two names that may be one type through a class that inherits
|
|
181
|
+
// it, or a name that leaves the surrounding arguments unsaid:
|
|
182
|
+
// those arguments cannot be set side by side.
|
|
183
|
+
if (head !== SAME || !a.exact || !b.exact) {
|
|
184
|
+
return a.exact && b.exact && aroundA.length === 0 && aroundB.length === 0 ? every([head, own]) : UNKNOWN;
|
|
185
|
+
}
|
|
186
|
+
if (aroundA.length !== aroundB.length) return UNKNOWN;
|
|
187
|
+
return every([own, ...aroundA.map((each, i) => compareTypes(each, aroundB[i]))]);
|
|
188
|
+
}
|
|
189
|
+
default: return UNKNOWN;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @param {object} index
|
|
195
|
+
* @param {object} world - what the test model knows of classes:
|
|
196
|
+
* `classes(filePath)` the reader's class rows of a file,
|
|
197
|
+
* `parents(filePath, cls)` the class each base of a class row denotes
|
|
198
|
+
* ({ filePath, cls }) or an opaque entry, `unitOf(filePath)` what
|
|
199
|
+
* the file is compiled with (two files of one unit see one set of
|
|
200
|
+
* packages), or null, and `aliasTerm(filePath, line)` the type a using
|
|
201
|
+
* alias directive on that line writes, as the reader gives it.
|
|
202
|
+
*/
|
|
203
|
+
function createSignatures(index, world) {
|
|
204
|
+
const memo = new Map();
|
|
205
|
+
const relations = new Map();
|
|
206
|
+
const remember = (key, compute) => {
|
|
207
|
+
if (memo.has(key)) return memo.get(key);
|
|
208
|
+
const value = compute();
|
|
209
|
+
memo.set(key, value);
|
|
210
|
+
return value;
|
|
211
|
+
};
|
|
212
|
+
const languageOf = filePath => index.files.get(filePath)?.language;
|
|
213
|
+
|
|
214
|
+
/** The project's type declarations of a simple name, in one language. */
|
|
215
|
+
const typeDefs = (name, language) => remember(`defs\0${language}\0${name}`, () =>
|
|
216
|
+
(index.symbols.get(name) || []).filter(def => def && def.name === name && TYPE_KINDS.has(def.type) &&
|
|
217
|
+
def.file && languageOf(def.file) === language));
|
|
218
|
+
|
|
219
|
+
const identity = language => !!langTraits(language)?.genericArityIsIdentity;
|
|
220
|
+
const arityOf = def => def.typeArity || 0;
|
|
221
|
+
/** `Name` or, where arity is part of a type's identity and it takes type arguments, `Name\`2`. */
|
|
222
|
+
const spelled = (name, arity, language) => (identity(language) && arity ? `${name}\`${arity}` : name);
|
|
223
|
+
/** Does one declaration stand inside another (by offsets where both have them: two can share a line)? */
|
|
224
|
+
const inside = (inner, outer) => (inner.sourceSpan && outer.sourceSpan
|
|
225
|
+
? outer.sourceSpan.start <= inner.sourceSpan.start && outer.sourceSpan.end >= inner.sourceSpan.end
|
|
226
|
+
: outer.startLine <= inner.startLine && (outer.endLine ?? outer.startLine) >= (inner.endLine ?? inner.startLine));
|
|
227
|
+
/** The type declarations a declaration stands in, outermost first (a name alone where one is not read). */
|
|
228
|
+
const ownersOf = def => {
|
|
229
|
+
if (!def.enclosingType) return [];
|
|
230
|
+
return remember(`owners\0${def.file}\0${def.startLine}\0${def.sourceSpan ? def.sourceSpan.start : ''}\0${def.name}`, () => {
|
|
231
|
+
const around = (index.symbols.get(def.enclosingType) || [])
|
|
232
|
+
.filter(other => other !== def && TYPE_KINDS.has(other.type) && other.file === def.file && inside(def, other))
|
|
233
|
+
.sort((a, b) => (b.sourceSpan && a.sourceSpan ? b.sourceSpan.start - a.sourceSpan.start : b.startLine - a.startLine))[0];
|
|
234
|
+
return around ? [...ownersOf(around), around] : [{ name: def.enclosingType }];
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
/**
|
|
238
|
+
* A declaration's canonical name: namespace (package), the types around
|
|
239
|
+
* it, its name; each type with its arity where that is part of its
|
|
240
|
+
* identity (`N.Outer\`1.Inner\`1` is not `N.Outer\`2.Inner`).
|
|
241
|
+
*/
|
|
242
|
+
const ownerFqOf = def => {
|
|
243
|
+
const language = languageOf(def.file);
|
|
244
|
+
return [def.namespace, ...ownersOf(def).map(owner => spelled(owner.name, arityOf(owner), language))].filter(Boolean).join('.');
|
|
245
|
+
};
|
|
246
|
+
const fqOf = def => [ownerFqOf(def), spelled(def.name, arityOf(def), languageOf(def.file))].filter(Boolean).join('.');
|
|
247
|
+
/** How many type arguments the types around a declaration take together. */
|
|
248
|
+
const outerArityOf = def => ownersOf(def).reduce((sum, owner) => sum + arityOf(owner), 0);
|
|
249
|
+
/** Does a declaration take as many type arguments as were written? Compared only where arity is identity. */
|
|
250
|
+
const arityFits = (def, arity, language) => arity == null || !identity(language) || arityOf(def) === arity;
|
|
251
|
+
const distinct = defs => [...new Map(defs.map(def => [fqOf(def), def])).values()];
|
|
252
|
+
|
|
253
|
+
const defOfRow = (filePath, cls) => (index.symbols.get(cls.name) || []).find(def =>
|
|
254
|
+
def.file === filePath && def.startLine === cls.startLine && TYPE_KINDS.has(def.type)) || null;
|
|
255
|
+
const rowOfDef = def => (world.classes(def.file) || []).find(cls =>
|
|
256
|
+
cls.name === def.name && cls.startLine === def.startLine) || null;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The member types of one name a type has: its own (which hide the
|
|
260
|
+
* rest), else the ones it inherits. Java inherits them from the
|
|
261
|
+
* superclass and the superinterfaces; C# from the base class only.
|
|
262
|
+
* `tags` name the bases that are not read: each may declare one.
|
|
263
|
+
*/
|
|
264
|
+
const memberTypes = (def, name, trail = new Set()) => {
|
|
265
|
+
const language = languageOf(def.file);
|
|
266
|
+
const key = `members\0${fqOf(def)}\0${name}`;
|
|
267
|
+
if (memo.has(key)) return memo.get(key);
|
|
268
|
+
if (trail.has(key)) return { found: [], tags: [] };
|
|
269
|
+
const mine = fqOf(def);
|
|
270
|
+
const own = typeDefs(name, language).filter(other => other.enclosingType === def.name && ownerFqOf(other) === mine);
|
|
271
|
+
let answer;
|
|
272
|
+
if (own.length > 0) {
|
|
273
|
+
answer = { found: distinct(own), tags: [], own: true };
|
|
274
|
+
} else {
|
|
275
|
+
const found = [];
|
|
276
|
+
const tags = new Set();
|
|
277
|
+
const next = new Set(trail).add(key);
|
|
278
|
+
const interfacesPass = langTraits(language)?.typeNameScoping === 'package-on-demand';
|
|
279
|
+
// Every part of a partial type writes bases of its own.
|
|
280
|
+
const parts = typeDefs(def.name, language).filter(other => fqOf(other) === fqOf(def));
|
|
281
|
+
for (const part of parts) {
|
|
282
|
+
const row = rowOfDef(part);
|
|
283
|
+
if (!row) {
|
|
284
|
+
// Its bases are not read here.
|
|
285
|
+
if ([part.extends, part.implements].flat().filter(Boolean).length > 0) tags.add(`${fqOf(part)}>?`);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
world.parents(part.file, row).forEach(({ written, node }, i) => {
|
|
289
|
+
if (node.opaque) {
|
|
290
|
+
if (interfacesPass || i === 0) tags.add(`${fqOf(part)}>${written}`);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const parent = defOfRow(node.filePath, node.cls);
|
|
294
|
+
if (!parent) {
|
|
295
|
+
tags.add(`${fqOf(part)}>${written}`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (!interfacesPass && (i > 0 || parent.type === 'interface')) return;
|
|
299
|
+
const above = memberTypes(parent, name, next);
|
|
300
|
+
// A private member type is not inherited.
|
|
301
|
+
found.push(...above.found.filter(other => !(other.modifiers || []).includes('private')));
|
|
302
|
+
for (const tag of above.tags) tags.add(tag);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
answer = { found: distinct(found), tags: [...tags].sort() };
|
|
306
|
+
}
|
|
307
|
+
if (trail.size === 0) memo.set(key, answer);
|
|
308
|
+
return answer;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/** The project type a canonical name is, or null. */
|
|
312
|
+
const byCanonical = (fq, language) => typeDefs(lastOf(fq), language).find(def => fqOf(def) === fq) || null;
|
|
313
|
+
|
|
314
|
+
/** Walk the member types a qualified name goes through after its head. */
|
|
315
|
+
const descend = (def, rest, language, implicit = false) => {
|
|
316
|
+
let current = def;
|
|
317
|
+
let through = implicit;
|
|
318
|
+
const tags = new Set();
|
|
319
|
+
for (const segment of rest) {
|
|
320
|
+
const below = memberTypes(current, segment.name);
|
|
321
|
+
const fits = below.found.filter(other => arityFits(other, segment.arity, language));
|
|
322
|
+
if (fits.length !== 1) {
|
|
323
|
+
for (const tag of below.tags) tags.add(tag);
|
|
324
|
+
return { open: true, tags: [...tags] };
|
|
325
|
+
}
|
|
326
|
+
// Written through a class that inherits it: the type arguments
|
|
327
|
+
// written on that class are not the declaring type's.
|
|
328
|
+
if (!below.own) through = true;
|
|
329
|
+
current = fits[0];
|
|
330
|
+
}
|
|
331
|
+
return { def: current, ...(through && { implicit: true }) };
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* A dotted name read from the root: the longest package (namespace)
|
|
336
|
+
* the project declares a type of that name in, then the types inside
|
|
337
|
+
* it. Null when the project declares none.
|
|
338
|
+
*/
|
|
339
|
+
const fromRoot = (segments, language) => {
|
|
340
|
+
for (let k = segments.length - 1; k >= 0; k--) {
|
|
341
|
+
const namespace = segments.slice(0, k).map(segment => segment.name).join('.');
|
|
342
|
+
const top = typeDefs(segments[k].name, language).filter(def => !def.enclosingType &&
|
|
343
|
+
(def.namespace || '') === namespace && arityFits(def, segments[k].arity, language));
|
|
344
|
+
if (top.length === 0) continue;
|
|
345
|
+
return descend(top[0], segments.slice(k + 1), language);
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
/** A written name, each part with the number of type arguments written on it where arity is identity. */
|
|
351
|
+
const dotted = (segments, language) => segments.map(segment => spelled(segment.name, segment.arity || 0, language)).join('.');
|
|
352
|
+
|
|
353
|
+
/** The roots a qualified name can start at without naming a type: the platform's, the project's packages, the file's imports. */
|
|
354
|
+
const rootsOf = (filePath, language) => remember(`roots\0${filePath}`, () => {
|
|
355
|
+
const roots = new Set(langTraits(language)?.standardPathRoots || []);
|
|
356
|
+
for (const namespace of projectNamespaces(index, language)) roots.add(String(namespace).split('.')[0]);
|
|
357
|
+
for (const detail of index.files.get(filePath)?.importDetails || []) {
|
|
358
|
+
if (detail.module) roots.add(String(detail.module).replace(/^global::/, '').split('.')[0]);
|
|
359
|
+
}
|
|
360
|
+
return roots;
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// ---- JLS 6.4.1 ---------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
const javaFileLevel = (site, segments, language) => {
|
|
366
|
+
const [head, ...rest] = segments;
|
|
367
|
+
const name = head.name;
|
|
368
|
+
const entry = index.files.get(site.filePath);
|
|
369
|
+
const details = entry?.importDetails || [];
|
|
370
|
+
const traits = langTraits(language);
|
|
371
|
+
const finish = hit => {
|
|
372
|
+
if (hit.def) return rest.length > 0 ? descend(hit.def, rest, language) : hit;
|
|
373
|
+
if (hit.ext) return rest.length > 0 ? { ext: `${hit.ext}.${dotted(rest, language)}`, canonical: false } : hit;
|
|
374
|
+
return hit;
|
|
375
|
+
};
|
|
376
|
+
// The compilation unit's own types.
|
|
377
|
+
const here = typeDefs(name, language).filter(def => def.file === site.filePath && !def.enclosingType);
|
|
378
|
+
if (here.length > 0) return finish({ def: here[0] });
|
|
379
|
+
// Single-type imports: a canonical name.
|
|
380
|
+
const single = details.find(detail => detail.type === 'import' && detail.module &&
|
|
381
|
+
!/\.\*$/.test(detail.module) && lastOf(detail.module) === name);
|
|
382
|
+
if (single) {
|
|
383
|
+
const def = byCanonical(single.module, language);
|
|
384
|
+
return finish(def ? { def } : { ext: single.module, canonical: true });
|
|
385
|
+
}
|
|
386
|
+
// A static import brings a member type of that name too.
|
|
387
|
+
for (const detail of details) {
|
|
388
|
+
if (detail.type !== 'static' || !detail.module || /\.\*$/.test(detail.module) || lastOf(detail.module) !== name) continue;
|
|
389
|
+
const def = byCanonical(detail.module, language);
|
|
390
|
+
if (def) return finish({ def });
|
|
391
|
+
const owner = byCanonical(detail.module.slice(0, -(name.length + 1)), language);
|
|
392
|
+
if (!owner) return { open: true, named: true };
|
|
393
|
+
const inherited = memberTypes(owner, name);
|
|
394
|
+
if (inherited.found.length === 1) return finish({ def: inherited.found[0] });
|
|
395
|
+
if (inherited.found.length > 1 || inherited.tags.length > 0) return { open: true, named: true };
|
|
396
|
+
}
|
|
397
|
+
const pkg = siteNamespaceOf(entry, site.line);
|
|
398
|
+
if (pkg === undefined) return { open: true, named: true };
|
|
399
|
+
const same = typeDefs(name, language).filter(def => !def.enclosingType && (def.namespace || '') === pkg);
|
|
400
|
+
if (same.length > 0) return finish({ def: same[0] });
|
|
401
|
+
// On-demand imports and the implicit one: a name that compiles has
|
|
402
|
+
// one provider among them.
|
|
403
|
+
const onDemand = details.filter(detail => detail.module && /\.\*$/.test(detail.module))
|
|
404
|
+
.map(detail => ({ module: detail.module.slice(0, -2), fromType: detail.type === 'static' }));
|
|
405
|
+
const candidates = [];
|
|
406
|
+
let unseen = false;
|
|
407
|
+
for (const each of onDemand) {
|
|
408
|
+
if (!each.fromType) {
|
|
409
|
+
candidates.push(...typeDefs(name, language).filter(def => !def.enclosingType && def.namespace === each.module));
|
|
410
|
+
}
|
|
411
|
+
const owner = byCanonical(each.module, language);
|
|
412
|
+
if (owner) {
|
|
413
|
+
const inside = memberTypes(owner, name);
|
|
414
|
+
candidates.push(...inside.found);
|
|
415
|
+
if (inside.tags.length > 0) unseen = true;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const found = distinct(candidates);
|
|
419
|
+
const implicit = traits?.implicitTypeImport && traits.implicitTypeImport.names.has(name)
|
|
420
|
+
? `${traits.implicitTypeImport.module}.${name}` : null;
|
|
421
|
+
if (found.length + (implicit ? 1 : 0) === 1 && !unseen) {
|
|
422
|
+
return finish(found.length === 1 ? { def: found[0] } : { ext: implicit, canonical: true });
|
|
423
|
+
}
|
|
424
|
+
if (found.length > 0 || implicit) return { open: true, named: true };
|
|
425
|
+
// No type in scope under that name. A qualified name starts at a package.
|
|
426
|
+
if (rest.length > 0) {
|
|
427
|
+
const absolute = fromRoot(segments, language);
|
|
428
|
+
if (absolute) return absolute;
|
|
429
|
+
if (rootsOf(site.filePath, language).has(name)) return { ext: dotted(segments, language), canonical: false };
|
|
430
|
+
}
|
|
431
|
+
const packages = projectNamespaces(index, language);
|
|
432
|
+
const providers = onDemand.map(each => each.module);
|
|
433
|
+
if (traits?.implicitTypeImport) providers.push(traits.implicitTypeImport.module);
|
|
434
|
+
return {
|
|
435
|
+
open: true, named: true,
|
|
436
|
+
scope: {
|
|
437
|
+
base: `package:${pkg}`, providers,
|
|
438
|
+
usings: providers.filter(module => !packages.has(module) && !byCanonical(module, language)).sort(),
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// ---- C#: namespace and type names --------------------------------------
|
|
444
|
+
|
|
445
|
+
/** The using directive that declares an alias in effect at a namespace level: its file and line, or null. */
|
|
446
|
+
const aliasDirective = (filePath, name, level, language) => {
|
|
447
|
+
const declares = detail => detail.type === 'using' && (detail.names || [])[0] === name && detail.module;
|
|
448
|
+
const own = (index.files.get(filePath)?.importDetails || []).filter(detail => declares(detail) &&
|
|
449
|
+
(detail.global ? level === '' : (detail.namespace || '') === level));
|
|
450
|
+
if (own.length === 1) return { filePath, line: own[0].line };
|
|
451
|
+
if (own.length > 1 || level !== '') return null;
|
|
452
|
+
// A `global using` of another file of the project.
|
|
453
|
+
const elsewhere = remember(`global-alias\0${language}\0${name}`, () => {
|
|
454
|
+
const out = [];
|
|
455
|
+
for (const [otherPath, entry] of index.files) {
|
|
456
|
+
if (entry.language !== language) continue;
|
|
457
|
+
for (const detail of entry.importDetails || []) {
|
|
458
|
+
if (detail.global && declares(detail)) out.push({ filePath: otherPath, line: detail.line });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return out;
|
|
462
|
+
});
|
|
463
|
+
return elsewhere.length === 1 ? elsewhere[0] : null;
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* What a using alias stands for: the type its directive writes, read
|
|
468
|
+
* where the directive stands (an alias never sees the aliases of its
|
|
469
|
+
* own namespace body). `Alias.Member` continues a plain name only. An
|
|
470
|
+
* alias a project file declares is a dotted name read from the root.
|
|
471
|
+
*/
|
|
472
|
+
const aliasTarget = (site, name, level, rest, language, syntax) => {
|
|
473
|
+
const unread = { open: true, named: false };
|
|
474
|
+
const directive = aliasDirective(site.filePath, name, level, language);
|
|
475
|
+
if (!directive) {
|
|
476
|
+
const text = String(syntax || '').replace(/^global::/, '');
|
|
477
|
+
if (!/^[A-Za-z_]\w*(\.[A-Za-z_]\w*)+$/.test(text)) return unread;
|
|
478
|
+
const segments = [...text.split('.').map(part => ({ name: part })), ...rest];
|
|
479
|
+
return fromRoot(segments, language) || { ext: dotted(segments, language), canonical: false };
|
|
480
|
+
}
|
|
481
|
+
const term = world.aliasTerm ? world.aliasTerm(directive.filePath, directive.line) : null;
|
|
482
|
+
if (!term || (site.aliasDepth || 0) >= 4) return unread;
|
|
483
|
+
const at = {
|
|
484
|
+
filePath: directive.filePath, line: directive.line, chain: [], typeParams: [],
|
|
485
|
+
namespace: level, skipAliasesAt: level, aliasDepth: (site.aliasDepth || 0) + 1,
|
|
486
|
+
};
|
|
487
|
+
const plain = !!term.path && term.path.every(segment => !(segment.args || []).length);
|
|
488
|
+
if (plain) {
|
|
489
|
+
const segments = [...term.path.map(segment => ({ name: segment.name, arity: 0 })), ...rest];
|
|
490
|
+
return lookup(at, segments, term.global === true);
|
|
491
|
+
}
|
|
492
|
+
return rest.length > 0 ? unread : { term: resolve(term, at) };
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const csharpFileLevel = (site, segments, language) => {
|
|
496
|
+
const [head, ...rest] = segments;
|
|
497
|
+
const name = head.name;
|
|
498
|
+
const entry = index.files.get(site.filePath);
|
|
499
|
+
const namespace = site.namespace !== undefined ? site.namespace : siteNamespaceOf(entry, site.line);
|
|
500
|
+
if (namespace === undefined) return { open: true, named: false };
|
|
501
|
+
const scope = csharpUsings(index, site.filePath, namespace);
|
|
502
|
+
const declared = csharpDeclaredNames(index);
|
|
503
|
+
const levels = [];
|
|
504
|
+
for (let parts = namespace ? namespace.split('.') : []; parts.length > 0; parts = parts.slice(0, -1)) levels.push(parts.join('.'));
|
|
505
|
+
levels.push('');
|
|
506
|
+
const finish = hit => (hit.def && rest.length > 0 ? descend(hit.def, rest, language) : hit);
|
|
507
|
+
const declaredAt = (origins, target, level) => (origins.get(target) || new Set()).has(level);
|
|
508
|
+
// An inner namespace's using directive names a namespace this
|
|
509
|
+
// cannot read: a type found further out may be hidden by it.
|
|
510
|
+
let behind = false;
|
|
511
|
+
for (const level of levels) {
|
|
512
|
+
const members = typeDefs(name, language).filter(def => !def.enclosingType &&
|
|
513
|
+
(def.namespace || '') === level && arityFits(def, head.arity, language));
|
|
514
|
+
if (members.length > 0) return behind ? { open: true, named: true } : finish({ def: members[0] });
|
|
515
|
+
// A namespace of that name: the rest is read from it.
|
|
516
|
+
const nested = level ? `${level}.${name}` : name;
|
|
517
|
+
if (rest.length > 0 && declared.namespaces.has(nested)) {
|
|
518
|
+
if (behind) return { open: true, named: true };
|
|
519
|
+
const absolute = [...(level ? level.split('.').map(part => ({ name: part })) : []), ...segments];
|
|
520
|
+
return fromRoot(absolute, language) || { ext: dotted(absolute, language), canonical: false };
|
|
521
|
+
}
|
|
522
|
+
if (site.skipAliasesAt !== level && scope.aliases.has(name) &&
|
|
523
|
+
((scope.aliasScopes && scope.aliasScopes.get(name)) || '') === level) {
|
|
524
|
+
if (behind || scope.ambiguousAliases.has(name) || head.arity) return { open: true, named: false };
|
|
525
|
+
return aliasTarget(site, name, level, rest, language, scope.aliasSyntax.get(name) || scope.aliases.get(name));
|
|
526
|
+
}
|
|
527
|
+
const usings = [...scope.namespaces].filter(target => declaredAt(scope.namespaceScopes, target, level));
|
|
528
|
+
const statics = [...scope.statics].filter(target => declaredAt(scope.staticScopes, target, level));
|
|
529
|
+
const brought = [];
|
|
530
|
+
let unseen = false;
|
|
531
|
+
brought.push(...typeDefs(name, language).filter(def => !def.enclosingType &&
|
|
532
|
+
usings.includes(def.namespace || '') && arityFits(def, head.arity, language)));
|
|
533
|
+
for (const target of statics) {
|
|
534
|
+
const owner = byCanonical(target, language);
|
|
535
|
+
if (!owner) continue;
|
|
536
|
+
const inside = memberTypes(owner, name);
|
|
537
|
+
brought.push(...inside.found.filter(def => arityFits(def, head.arity, language)));
|
|
538
|
+
if (inside.tags.length > 0) unseen = true;
|
|
539
|
+
}
|
|
540
|
+
const found = distinct(brought);
|
|
541
|
+
if (found.length === 1 && !unseen) return behind ? { open: true, named: true } : finish({ def: found[0] });
|
|
542
|
+
if (found.length > 1 || unseen) return { open: true, named: true };
|
|
543
|
+
if (level !== '' && (usings.some(target => !declared.namespaces.has(target)) ||
|
|
544
|
+
statics.some(target => !declared.types.has(target)))) behind = true;
|
|
545
|
+
}
|
|
546
|
+
// No type of the project in scope under that name. A qualified
|
|
547
|
+
// name that starts at a namespace is read from the root.
|
|
548
|
+
if (rest.length > 0 && !behind && rootsOf(site.filePath, language).has(name)) {
|
|
549
|
+
return { ext: dotted(segments, language), canonical: false };
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
open: true, named: !scope.unreadable,
|
|
553
|
+
scope: {
|
|
554
|
+
base: `namespace:${namespace}`, providers: [...scope.namespaces],
|
|
555
|
+
usings: [...[...scope.namespaces].filter(target => !declared.namespaces.has(target)),
|
|
556
|
+
...[...scope.statics].filter(target => !declared.types.has(target))].sort(),
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* What the head of a written name denotes at a site and, through it,
|
|
563
|
+
* the whole name: { def }, { ext, canonical }, { variable }, { term }
|
|
564
|
+
* (an alias of a type that is no plain name), or { open, named,
|
|
565
|
+
* scope }. `tags`: the unread bases passed on the way.
|
|
566
|
+
*/
|
|
567
|
+
const lookup = (site, segments, fromRootOnly) => {
|
|
568
|
+
const language = languageOf(site.filePath);
|
|
569
|
+
const scoping = langTraits(language)?.typeNameScoping;
|
|
570
|
+
if (!scoping) return { open: true, named: false };
|
|
571
|
+
if (fromRootOnly) return fromRoot(segments, language) || { ext: dotted(segments, language), canonical: false };
|
|
572
|
+
const [head, ...rest] = segments;
|
|
573
|
+
const variable = (site.typeParams || []).findIndex(each => each.name === head.name);
|
|
574
|
+
if (variable !== -1 && rest.length === 0 && !head.arity) return { variable, bound: site.typeParams[variable].bound || null };
|
|
575
|
+
const tags = new Set();
|
|
576
|
+
for (let i = site.chain.length - 1; i >= 0; i--) {
|
|
577
|
+
const cls = site.chain[i];
|
|
578
|
+
if ((cls.typeParams || []).includes(head.name) && rest.length === 0) return { open: true, named: false };
|
|
579
|
+
const def = defOfRow(site.filePath, cls);
|
|
580
|
+
if (!def) {
|
|
581
|
+
tags.add(`${site.filePath}:${cls.startLine}>?`);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
const inside = memberTypes(def, head.name);
|
|
585
|
+
const fits = inside.found.filter(other => arityFits(other, head.arity, language));
|
|
586
|
+
if (fits.length === 1) {
|
|
587
|
+
// Named from inside the classes around it: the type
|
|
588
|
+
// arguments of those classes are not written.
|
|
589
|
+
const hit = rest.length > 0 ? descend(fits[0], rest, language, true) : { def: fits[0], implicit: true };
|
|
590
|
+
return { ...hit, tags: [...new Set([...tags, ...(hit.tags || [])])].sort() };
|
|
591
|
+
}
|
|
592
|
+
if (fits.length > 1) return { open: true, named: true };
|
|
593
|
+
for (const tag of inside.tags) tags.add(tag);
|
|
594
|
+
}
|
|
595
|
+
const hit = scoping === 'package-on-demand' ? javaFileLevel(site, segments, language) : csharpFileLevel(site, segments, language);
|
|
596
|
+
return { ...hit, tags: [...new Set([...tags, ...(hit.tags || [])])].sort() };
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
/** A written type, resolved. */
|
|
600
|
+
const resolve = (term, site) => {
|
|
601
|
+
if (!term) return null;
|
|
602
|
+
const language = languageOf(site.filePath);
|
|
603
|
+
const unit = world.unitOf ? world.unitOf(site.filePath) : null;
|
|
604
|
+
if (term.id) return { fq: term.id, last: lastOf(term.id), value: term.value, canonical: true, unit };
|
|
605
|
+
if (term.unread != null) return { open: true, named: false, last: null, text: String(term.unread), scope: null };
|
|
606
|
+
if (term.array) return { array: resolve(term.array, site), rank: term.rank || 1 };
|
|
607
|
+
if (term.pointer) return { pointer: resolve(term.pointer, site) };
|
|
608
|
+
if (term.tuple) return { tuple: term.tuple.map(each => resolve(each, site)) };
|
|
609
|
+
if (term.nullable) return nullableOf(resolve(term.nullable, site));
|
|
610
|
+
if (!term.path || term.path.length === 0) return { open: true, named: false, last: null, text: '', scope: null };
|
|
611
|
+
const segments = term.path.map(segment => ({ name: segment.name, arity: (segment.args || []).length || 0 }));
|
|
612
|
+
const args = term.path.flatMap(segment => segment.args || []);
|
|
613
|
+
const text = dotted(segments, language);
|
|
614
|
+
const hit = lookup(site, segments, term.global === true);
|
|
615
|
+
// An alias of a type that is no plain name: the type itself.
|
|
616
|
+
if (hit.term) return args.length === 0 ? hit.term : { open: true, named: false, last: null, text, scope: null };
|
|
617
|
+
const unless = hit.tags && hit.tags.length > 0 ? hit.tags.join('\n') : null;
|
|
618
|
+
let head;
|
|
619
|
+
if (hit.variable != null) {
|
|
620
|
+
if (langTraits(language)?.genericArityIsIdentity) return { variable: hit.variable };
|
|
621
|
+
// A method's type parameter is its bound where signatures are
|
|
622
|
+
// compared by erasure (JLS 8.4.2).
|
|
623
|
+
if (hit.bound) return resolve(hit.bound, { ...site, typeParams: site.typeParams.filter((_, i) => i !== hit.variable) });
|
|
624
|
+
const traits = langTraits(language);
|
|
625
|
+
const top = traits?.implicitTypeImport && traits.universalSupertype
|
|
626
|
+
? `${traits.implicitTypeImport.module}.${traits.universalSupertype}` : null;
|
|
627
|
+
return top ? { fq: top, last: lastOf(top), value: false, canonical: true, unit }
|
|
628
|
+
: { open: true, named: false, last: null, text, scope: null };
|
|
629
|
+
}
|
|
630
|
+
// The type arguments written on the name's last part are the
|
|
631
|
+
// type's own; the ones before it belong to the types around it.
|
|
632
|
+
const ownCount = (term.path[term.path.length - 1].args || []).length;
|
|
633
|
+
const resolved = args.map(each => resolve(each, site));
|
|
634
|
+
const own = resolved.slice(resolved.length - ownCount);
|
|
635
|
+
const around = resolved.slice(0, resolved.length - ownCount);
|
|
636
|
+
if (hit.def) {
|
|
637
|
+
head = {
|
|
638
|
+
fq: fqOf(hit.def), last: spelled(hit.def.name, arityOf(hit.def), language), canonical: true, project: true, text, unit,
|
|
639
|
+
...(VALUE_KINDS.has(hit.def.type) && { value: true }),
|
|
640
|
+
...(REFERENCE_KINDS.has(hit.def.type) && { value: false }),
|
|
641
|
+
...(unless && { unless }),
|
|
642
|
+
};
|
|
643
|
+
if (!identity(language)) return head;
|
|
644
|
+
// The types around a declared type take arguments too. They
|
|
645
|
+
// are compared where each is written on the type that
|
|
646
|
+
// declares it; a name written from inside those types, or
|
|
647
|
+
// through a class that inherits the member, leaves them
|
|
648
|
+
// unsaid (`exact` false).
|
|
649
|
+
const needed = outerArityOf(hit.def);
|
|
650
|
+
if (own.length === 0 && needed === 0) return head;
|
|
651
|
+
const exact = needed === 0 || (!hit.implicit && around.length === needed);
|
|
652
|
+
return { generic: head, args: [...(needed > 0 && exact ? around : []), ...own], own: own.length, exact };
|
|
653
|
+
}
|
|
654
|
+
if (hit.ext) {
|
|
655
|
+
head = { fq: hit.ext, last: lastOf(hit.ext), canonical: hit.canonical !== false, text, unit, ...(unless && { unless }) };
|
|
656
|
+
} else {
|
|
657
|
+
head = {
|
|
658
|
+
open: true, named: hit.named === true,
|
|
659
|
+
last: spelled(segments[segments.length - 1].name, segments[segments.length - 1].arity, language), text,
|
|
660
|
+
simple: segments.length === 1,
|
|
661
|
+
scope: hit.scope ? {
|
|
662
|
+
base: `${hit.scope.base}\0${unless || ''}`, usings: hit.scope.usings,
|
|
663
|
+
providers: hit.scope.providers || [], guarded: !!unless, unit,
|
|
664
|
+
} : null,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
if (resolved.length === 0) return head;
|
|
668
|
+
// The platform's generic types a language has a syntax of its own
|
|
669
|
+
// for: `System.Nullable<T>` is `T?`, `System.ValueTuple<A, B>` is `(A, B)`.
|
|
670
|
+
const forms = langTraits(language)?.platformTypeForms;
|
|
671
|
+
if (forms && head.fq && !head.unless && around.length === 0) {
|
|
672
|
+
if (head.fq === spelled(forms.nullable, 1, language) && own.length === 1) return nullableOf(own[0]);
|
|
673
|
+
if (head.fq === spelled(forms.tuple, own.length, language) && own.length >= 2 && own.length <= 7) return { tuple: own };
|
|
674
|
+
}
|
|
675
|
+
// Two spellings of one name in one scope put the same arguments
|
|
676
|
+
// on the same parts.
|
|
677
|
+
return { generic: head, args: resolved, own: own.length, exact: true };
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
/** `T?`: another type over a value type, T itself over a reference type (the mark is an annotation there). */
|
|
681
|
+
const nullableOf = inner => {
|
|
682
|
+
const value = valueOf(inner);
|
|
683
|
+
if (value === false) return inner;
|
|
684
|
+
return { nullable: inner, ...(value !== true && { maybe: true }) };
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
/** Is a resolved type a value type (true), a reference type (false), or not known? */
|
|
688
|
+
const valueOf = term => {
|
|
689
|
+
switch (kindOf(term)) {
|
|
690
|
+
case 'name': return term.unless ? undefined : term.value;
|
|
691
|
+
case 'array': return false;
|
|
692
|
+
case 'tuple': return true;
|
|
693
|
+
case 'generic': return term.generic.open || term.generic.unless ? undefined : term.generic.value;
|
|
694
|
+
default: return undefined;
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
/** The classes around a class row in its file, outermost first, the row last. */
|
|
699
|
+
const chainOf = (filePath, cls) => {
|
|
700
|
+
const suite = cls.suite || [];
|
|
701
|
+
const around = (world.classes(filePath) || []).filter(other => other !== cls &&
|
|
702
|
+
other.startLine <= cls.startLine && other.endLine >= cls.endLine &&
|
|
703
|
+
(other.suite || []).length < suite.length && suite[(other.suite || []).length] === other.name &&
|
|
704
|
+
(other.suite || []).every((name, i) => name === suite[i]));
|
|
705
|
+
return [...around.sort((a, b) => (a.suite || []).length - (b.suite || []).length), cls];
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Is the method `b` declares the method `a` declares: the same name
|
|
710
|
+
* and, parameter by parameter, the same type passed the same way.
|
|
711
|
+
* Each side: { filePath, cls, row } with the reader's `params`.
|
|
712
|
+
*/
|
|
713
|
+
const relate = (a, b) => {
|
|
714
|
+
if (a.row.name !== b.row.name) return DIFFERENT;
|
|
715
|
+
const pa = a.row.params;
|
|
716
|
+
const pb = b.row.params;
|
|
717
|
+
if (!Array.isArray(pa) || !Array.isArray(pb)) return UNKNOWN;
|
|
718
|
+
if (pa.length !== pb.length) return DIFFERENT;
|
|
719
|
+
const language = languageOf(a.filePath);
|
|
720
|
+
if (langTraits(language)?.genericArityIsIdentity &&
|
|
721
|
+
(a.row.typeParams || []).length !== (b.row.typeParams || []).length) return DIFFERENT;
|
|
722
|
+
if (pa.length === 0) return SAME;
|
|
723
|
+
// Remembered by the two parameter lists themselves: a reader gives
|
|
724
|
+
// each method one, whatever line it stands on.
|
|
725
|
+
if (!relations.has(pa)) relations.set(pa, new Map());
|
|
726
|
+
const known = relations.get(pa);
|
|
727
|
+
if (!known.has(pb)) {
|
|
728
|
+
const siteOf = side => ({
|
|
729
|
+
filePath: side.filePath, line: side.row.line, chain: chainOf(side.filePath, side.cls),
|
|
730
|
+
typeParams: side.row.typeParams || [],
|
|
731
|
+
});
|
|
732
|
+
const sa = siteOf(a);
|
|
733
|
+
const sb = siteOf(b);
|
|
734
|
+
known.set(pb, every(pa.map((term, i) => {
|
|
735
|
+
if ((term.by || '') !== (pb[i].by || '')) return DIFFERENT;
|
|
736
|
+
return compareTypes(resolve(term, sa), resolve(pb[i], sb));
|
|
737
|
+
})));
|
|
738
|
+
}
|
|
739
|
+
return known.get(pb);
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* The class a base written on a class row denotes, by the language's
|
|
744
|
+
* lookup: { filePath, cls }, null when it is no class of the project
|
|
745
|
+
* that is read (a type from outside, a name that is not resolved, a
|
|
746
|
+
* type declared in several parts), undefined when the language's
|
|
747
|
+
* lookup is not modeled here.
|
|
748
|
+
*/
|
|
749
|
+
const baseOf = (filePath, cls, term) => {
|
|
750
|
+
const language = languageOf(filePath);
|
|
751
|
+
if (!langTraits(language)?.typeNameScoping || !term) return undefined;
|
|
752
|
+
if (!term.path || term.path.length === 0) return null;
|
|
753
|
+
const segments = term.path.map(segment => ({ name: segment.name, arity: (segment.args || []).length || 0 }));
|
|
754
|
+
// A base is written outside the class's body, among its type parameters.
|
|
755
|
+
const site = {
|
|
756
|
+
filePath, line: cls.line, chain: chainOf(filePath, cls).slice(0, -1),
|
|
757
|
+
typeParams: [],
|
|
758
|
+
};
|
|
759
|
+
if ((cls.typeParams || []).includes(segments[0].name) && segments.length === 1) return null;
|
|
760
|
+
const hit = lookup(site, segments, term.global === true);
|
|
761
|
+
if (!hit.def || (hit.tags && hit.tags.length > 0)) return null;
|
|
762
|
+
const parts = typeDefs(hit.def.name, language).filter(other => fqOf(other) === fqOf(hit.def));
|
|
763
|
+
if (parts.length !== 1) return null;
|
|
764
|
+
const row = rowOfDef(hit.def);
|
|
765
|
+
return row ? { filePath: hit.def.file, cls: row } : null;
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
// The readers run under a guard (a shape that is not read is no
|
|
769
|
+
// answer); so does this. What is not read is not known: an inherited
|
|
770
|
+
// test then runs only sometimes, a base is no class that is read.
|
|
771
|
+
return {
|
|
772
|
+
relate(a, b) {
|
|
773
|
+
try {
|
|
774
|
+
return relate(a, b);
|
|
775
|
+
} catch (e) {
|
|
776
|
+
return UNKNOWN;
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
baseOf(filePath, cls, term) {
|
|
780
|
+
try {
|
|
781
|
+
return baseOf(filePath, cls, term);
|
|
782
|
+
} catch (e) {
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
module.exports = { createSignatures };
|