ucn 5.5.3 → 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.
Files changed (71) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -12
  2. package/.claude/skills/ucn/references/commands.md +18 -14
  3. package/.claude/skills/ucn/references/resolution-rules.md +4 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +4 -4
  5. package/CHANGELOG.md +474 -0
  6. package/README.md +69 -29
  7. package/cli/flags.js +184 -0
  8. package/cli/index.js +137 -324
  9. package/core/account.js +44 -87
  10. package/core/analysis.js +273 -107
  11. package/core/ast-analysis.js +2 -0
  12. package/core/bridge.js +14 -0
  13. package/core/cache.js +101 -16
  14. package/core/callers.js +352 -70
  15. package/core/capture.js +331 -0
  16. package/core/change-kinds.js +888 -0
  17. package/core/check.js +1117 -139
  18. package/core/command-contracts.js +7 -6
  19. package/core/constructed-class.js +1195 -0
  20. package/core/contract-membership.js +142 -0
  21. package/core/deadcode.js +362 -84
  22. package/core/deletion.js +401 -0
  23. package/core/discovery.js +104 -9
  24. package/core/entrypoints.js +12 -6
  25. package/core/evidence-bands.js +68 -0
  26. package/core/evidence-bands.json +44 -0
  27. package/core/execute.js +172 -161
  28. package/core/hot-summary.js +241 -0
  29. package/core/index-ir.js +3 -1
  30. package/core/ir.js +5 -1
  31. package/core/member-signatures.js +789 -0
  32. package/core/output/analysis-ext.js +20 -10
  33. package/core/output/analysis.js +98 -60
  34. package/core/output/check.js +98 -37
  35. package/core/output/doctor.js +34 -1
  36. package/core/output/find.js +2 -1
  37. package/core/output/lines.js +63 -3
  38. package/core/output/public.js +29 -11
  39. package/core/output/refactoring.js +32 -0
  40. package/core/output/reporting.js +15 -2
  41. package/core/output/search.js +11 -2
  42. package/core/output/tracing.js +35 -7
  43. package/core/output-budget.js +124 -57
  44. package/core/parallel-build.js +3 -3
  45. package/core/project.js +106 -23
  46. package/core/provenance-type.js +12 -5
  47. package/core/public-command.js +35 -9
  48. package/core/python-modules.js +6 -0
  49. package/core/reflection.js +151 -23
  50. package/core/registry.js +623 -72
  51. package/core/reporting.js +69 -34
  52. package/core/review-items.js +75 -0
  53. package/core/search.js +89 -128
  54. package/core/shared.js +0 -45
  55. package/core/snapshot.js +350 -0
  56. package/core/source-universe.js +707 -0
  57. package/core/test-units.js +3624 -0
  58. package/core/tracing.js +150 -96
  59. package/core/verify.js +27 -11
  60. package/languages/adapter.js +8 -3
  61. package/languages/c-family.js +12 -0
  62. package/languages/csharp.js +634 -0
  63. package/languages/go.js +120 -0
  64. package/languages/index.js +422 -4
  65. package/languages/java.js +712 -0
  66. package/languages/javascript.js +1460 -2
  67. package/languages/lexical-scope.js +68 -0
  68. package/languages/python.js +1672 -1
  69. package/languages/rust.js +91 -0
  70. package/mcp/server.js +44 -110
  71. package/package.json +10 -5
@@ -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 };