circle-ir 4.15.0 → 4.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/configs/sinks/golang.json +4 -4
  2. package/dist/analysis/config-loader.d.ts.map +1 -1
  3. package/dist/analysis/config-loader.js +11 -7
  4. package/dist/analysis/config-loader.js.map +1 -1
  5. package/dist/analysis/constant-caller-gate.d.ts +39 -0
  6. package/dist/analysis/constant-caller-gate.d.ts.map +1 -0
  7. package/dist/analysis/constant-caller-gate.js +298 -0
  8. package/dist/analysis/constant-caller-gate.js.map +1 -0
  9. package/dist/analysis/constant-propagation/evaluator.d.ts.map +1 -1
  10. package/dist/analysis/constant-propagation/evaluator.js +3 -1
  11. package/dist/analysis/constant-propagation/evaluator.js.map +1 -1
  12. package/dist/analysis/constant-propagation/patterns.d.ts +19 -0
  13. package/dist/analysis/constant-propagation/patterns.d.ts.map +1 -1
  14. package/dist/analysis/constant-propagation/patterns.js +33 -0
  15. package/dist/analysis/constant-propagation/patterns.js.map +1 -1
  16. package/dist/analysis/constant-propagation/propagator.d.ts +59 -0
  17. package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
  18. package/dist/analysis/constant-propagation/propagator.js +396 -49
  19. package/dist/analysis/constant-propagation/propagator.js.map +1 -1
  20. package/dist/analysis/constant-propagation/types.d.ts +9 -0
  21. package/dist/analysis/constant-propagation/types.d.ts.map +1 -1
  22. package/dist/analysis/findings.d.ts.map +1 -1
  23. package/dist/analysis/findings.js +134 -0
  24. package/dist/analysis/findings.js.map +1 -1
  25. package/dist/analysis/interprocedural.d.ts.map +1 -1
  26. package/dist/analysis/interprocedural.js +127 -3
  27. package/dist/analysis/interprocedural.js.map +1 -1
  28. package/dist/analysis/java-array-element-keys.d.ts +14 -0
  29. package/dist/analysis/java-array-element-keys.d.ts.map +1 -0
  30. package/dist/analysis/java-array-element-keys.js +36 -0
  31. package/dist/analysis/java-array-element-keys.js.map +1 -0
  32. package/dist/analysis/js-crlf-strip.d.ts +26 -0
  33. package/dist/analysis/js-crlf-strip.d.ts.map +1 -0
  34. package/dist/analysis/js-crlf-strip.js +244 -0
  35. package/dist/analysis/js-crlf-strip.js.map +1 -0
  36. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +25 -0
  37. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
  38. package/dist/analysis/passes/csharp-sanitizer-credit.js +157 -12
  39. package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
  40. package/dist/analysis/passes/language-sources-pass.d.ts +38 -3
  41. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  42. package/dist/analysis/passes/language-sources-pass.js +1234 -127
  43. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  44. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
  45. package/dist/analysis/passes/param-source-flow-gate-pass.js +53 -1
  46. package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
  47. package/dist/analysis/passes/plaintext-password-storage-pass.d.ts +6 -0
  48. package/dist/analysis/passes/plaintext-password-storage-pass.d.ts.map +1 -1
  49. package/dist/analysis/passes/plaintext-password-storage-pass.js +160 -0
  50. package/dist/analysis/passes/plaintext-password-storage-pass.js.map +1 -1
  51. package/dist/analysis/passes/scan-secrets-pass.d.ts +1 -1
  52. package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
  53. package/dist/analysis/passes/scan-secrets-pass.js +7 -1
  54. package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
  55. package/dist/analysis/passes/sink-filter-pass.d.ts +1 -1
  56. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  57. package/dist/analysis/passes/sink-filter-pass.js +326 -34
  58. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  59. package/dist/analysis/passes/taint-matcher-pass.d.ts.map +1 -1
  60. package/dist/analysis/passes/taint-matcher-pass.js +2 -2
  61. package/dist/analysis/passes/taint-matcher-pass.js.map +1 -1
  62. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  63. package/dist/analysis/passes/taint-propagation-pass.js +1376 -58
  64. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  65. package/dist/analysis/passes/weak-hash-pass.d.ts +6 -0
  66. package/dist/analysis/passes/weak-hash-pass.d.ts.map +1 -1
  67. package/dist/analysis/passes/weak-hash-pass.js +191 -4
  68. package/dist/analysis/passes/weak-hash-pass.js.map +1 -1
  69. package/dist/analysis/passes/weak-random-pass.d.ts +5 -0
  70. package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
  71. package/dist/analysis/passes/weak-random-pass.js +30 -0
  72. package/dist/analysis/passes/weak-random-pass.js.map +1 -1
  73. package/dist/analysis/project-callee-escape-gate.d.ts +40 -0
  74. package/dist/analysis/project-callee-escape-gate.d.ts.map +1 -0
  75. package/dist/analysis/project-callee-escape-gate.js +243 -0
  76. package/dist/analysis/project-callee-escape-gate.js.map +1 -0
  77. package/dist/analysis/rust-literal-guard-rebind.d.ts +34 -0
  78. package/dist/analysis/rust-literal-guard-rebind.d.ts.map +1 -0
  79. package/dist/analysis/rust-literal-guard-rebind.js +197 -0
  80. package/dist/analysis/rust-literal-guard-rebind.js.map +1 -0
  81. package/dist/analysis/rust-literal-guard.d.ts +33 -0
  82. package/dist/analysis/rust-literal-guard.d.ts.map +1 -0
  83. package/dist/analysis/rust-literal-guard.js +422 -0
  84. package/dist/analysis/rust-literal-guard.js.map +1 -0
  85. package/dist/analysis/scalar-derivation.d.ts +8 -0
  86. package/dist/analysis/scalar-derivation.d.ts.map +1 -1
  87. package/dist/analysis/scalar-derivation.js +172 -0
  88. package/dist/analysis/scalar-derivation.js.map +1 -1
  89. package/dist/analysis/taint-matcher.d.ts +2 -2
  90. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  91. package/dist/analysis/taint-matcher.js +531 -31
  92. package/dist/analysis/taint-matcher.js.map +1 -1
  93. package/dist/analysis/taint-propagation.d.ts +26 -1
  94. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  95. package/dist/analysis/taint-propagation.js +234 -13
  96. package/dist/analysis/taint-propagation.js.map +1 -1
  97. package/dist/analyzer.d.ts +16 -2
  98. package/dist/analyzer.d.ts.map +1 -1
  99. package/dist/analyzer.js +13 -3
  100. package/dist/analyzer.js.map +1 -1
  101. package/dist/browser/circle-ir.js +24432 -19928
  102. package/dist/core/circle-ir-core.cjs +4936 -3575
  103. package/dist/core/circle-ir-core.js +4936 -3575
  104. package/dist/core/extractors/calls.d.ts +12 -1
  105. package/dist/core/extractors/calls.d.ts.map +1 -1
  106. package/dist/core/extractors/calls.js +332 -22
  107. package/dist/core/extractors/calls.js.map +1 -1
  108. package/dist/core/extractors/dfg.js +102 -10
  109. package/dist/core/extractors/dfg.js.map +1 -1
  110. package/dist/core/extractors/types.d.ts +5 -1
  111. package/dist/core/extractors/types.d.ts.map +1 -1
  112. package/dist/core/extractors/types.js +62 -0
  113. package/dist/core/extractors/types.js.map +1 -1
  114. package/dist/graph/analysis-pass.d.ts +6 -1
  115. package/dist/graph/analysis-pass.d.ts.map +1 -1
  116. package/dist/graph/analysis-pass.js +2 -1
  117. package/dist/graph/analysis-pass.js.map +1 -1
  118. package/dist/navigation/navigation-index.d.ts +134 -14
  119. package/dist/navigation/navigation-index.d.ts.map +1 -1
  120. package/dist/navigation/navigation-index.js +1158 -104
  121. package/dist/navigation/navigation-index.js.map +1 -1
  122. package/dist/navigation/types.d.ts +59 -2
  123. package/dist/navigation/types.d.ts.map +1 -1
  124. package/dist/resolution/cross-file.d.ts +9 -0
  125. package/dist/resolution/cross-file.d.ts.map +1 -1
  126. package/dist/resolution/cross-file.js +102 -43
  127. package/dist/resolution/cross-file.js.map +1 -1
  128. package/dist/resolution/type-hierarchy.d.ts +3 -0
  129. package/dist/resolution/type-hierarchy.d.ts.map +1 -1
  130. package/dist/resolution/type-hierarchy.js +45 -1
  131. package/dist/resolution/type-hierarchy.js.map +1 -1
  132. package/dist/types/index.d.ts +33 -0
  133. package/dist/types/index.d.ts.map +1 -1
  134. package/dist/types/index.js.map +1 -1
  135. package/package.json +1 -1
@@ -28,9 +28,24 @@
28
28
  * repository, of which 7,246 are correctly unanswered (the target is outside
29
29
  * the tree) and 455 are these.
30
30
  *
31
- * What it deliberately does **not** do: invent a receiver type. Where no type
32
- * is known the answer is `inferred` or there is no answer, and `inferred` is a
33
- * permanent floor — see `Tier`.
31
+ * Two more, found by asking for the callers of overloaded and of overridden
32
+ * methods in a 6,900-file Java repository, where every such answer was wrong:
33
+ *
34
+ * - **Overloads.** A call binds to one overload, chosen the way the compiler
35
+ * chooses — by arity, then by argument types in the compiler's three
36
+ * phases, then the most specific — and only where the types read off the
37
+ * source make the choice certain. Where they do not, the site is
38
+ * `unresolved` with `overload-ambiguous` and the candidates. A symbol may
39
+ * carry a parameter list to ask about one overload; a query without one
40
+ * on an overloaded method gets its answers grouped per overload.
41
+ * - **Dispatch.** A call through a supertype's declaration can land on an
42
+ * override, and a call to an override is a caller of the operation it
43
+ * overrides. Both are answered `polymorphic`, with `via` naming what the
44
+ * site binds to, and the query's `scope` says what dispatch it cannot see.
45
+ *
46
+ * What it deliberately does **not** do: invent a receiver type, or an
47
+ * argument type. Where no type is known the answer is `inferred`, ambiguous,
48
+ * or absent, and `inferred` is a permanent floor — see `Tier`.
34
49
  */
35
50
  const REFLECTIVE_TYPES = new Set([
36
51
  'Class', 'Method', 'Field', 'Constructor', 'AccessibleObject',
@@ -43,8 +58,63 @@ const REFLECTIVE_METHODS = new Set([
43
58
  ]);
44
59
  const LOMBOK_ACCESSOR = new Set(['Getter', 'Setter', 'Data', 'Value']);
45
60
  const LOMBOK_BUILDER = new Set(['Builder', 'SuperBuilder']);
61
+ const PRIMITIVES = new Set(['byte', 'short', 'char', 'int', 'long', 'float', 'double', 'boolean']);
62
+ /** Primitive widening: what each primitive may be passed as, itself included. */
63
+ const WIDENS_TO = {
64
+ byte: ['byte', 'short', 'int', 'long', 'float', 'double'],
65
+ short: ['short', 'int', 'long', 'float', 'double'],
66
+ char: ['char', 'int', 'long', 'float', 'double'],
67
+ int: ['int', 'long', 'float', 'double'],
68
+ long: ['long', 'float', 'double'],
69
+ float: ['float', 'double'],
70
+ double: ['double'],
71
+ boolean: ['boolean'],
72
+ };
73
+ const BOXES = {
74
+ byte: 'Byte', short: 'Short', char: 'Character', int: 'Integer',
75
+ long: 'Long', float: 'Float', double: 'Double', boolean: 'Boolean',
76
+ };
77
+ const UNBOXES = Object.fromEntries(Object.entries(BOXES).map(([p, b]) => [b, p]));
78
+ /**
79
+ * JDK classes whose supertypes are known here, so a value of one can be ruled
80
+ * in or out of a parameter type without the JDK on hand. Every one is a class,
81
+ * not an interface; the `final` ones can only ever be themselves.
82
+ */
83
+ const JDK_SUPERS = (() => {
84
+ const num = ['Number', 'Comparable', 'Serializable', 'Object'];
85
+ const temporal = ['Temporal', 'TemporalAccessor', 'TemporalAdjuster', 'Comparable', 'Serializable', 'Object'];
86
+ return {
87
+ String: { final: true, supers: ['CharSequence', 'Comparable', 'Serializable', 'Constable', 'ConstantDesc', 'Object'] },
88
+ Long: { final: true, supers: num }, Integer: { final: true, supers: num },
89
+ Short: { final: true, supers: num }, Byte: { final: true, supers: num },
90
+ Double: { final: true, supers: num }, Float: { final: true, supers: num },
91
+ Boolean: { final: true, supers: ['Comparable', 'Serializable', 'Object'] },
92
+ Character: { final: true, supers: ['Comparable', 'Serializable', 'Object'] },
93
+ BigDecimal: { final: false, supers: num }, BigInteger: { final: false, supers: num },
94
+ LocalDate: { final: true, supers: ['ChronoLocalDate', ...temporal] },
95
+ LocalDateTime: { final: true, supers: ['ChronoLocalDateTime', ...temporal] },
96
+ LocalTime: { final: true, supers: temporal },
97
+ ZonedDateTime: { final: true, supers: ['ChronoZonedDateTime', ...temporal] },
98
+ OffsetDateTime: { final: true, supers: temporal },
99
+ Instant: { final: true, supers: temporal },
100
+ Duration: { final: true, supers: ['TemporalAmount', 'Comparable', 'Serializable', 'Object'] },
101
+ Period: { final: true, supers: ['ChronoPeriod', 'TemporalAmount', 'Serializable', 'Object'] },
102
+ UUID: { final: true, supers: ['Comparable', 'Serializable', 'Object'] },
103
+ Locale: { final: true, supers: ['Cloneable', 'Serializable', 'Object'] },
104
+ Class: { final: true, supers: ['Type', 'GenericDeclaration', 'AnnotatedElement', 'Serializable', 'Object'] },
105
+ Date: { final: false, supers: ['Cloneable', 'Comparable', 'Serializable', 'Object'] },
106
+ StringBuilder: { final: true, supers: ['CharSequence', 'Appendable', 'Comparable', 'Serializable', 'Object'] },
107
+ Object: { final: false, supers: [] },
108
+ };
109
+ })();
110
+ /** JDK interfaces with no supertype of their own. */
111
+ const LEAF_INTERFACES = new Set(['Serializable', 'Cloneable', 'Comparable', 'AutoCloseable', 'Runnable', 'Iterable', 'CharSequence']);
112
+ const JAVA_LANG = new Set([
113
+ 'String', 'Long', 'Integer', 'Short', 'Byte', 'Double', 'Float', 'Boolean', 'Character',
114
+ 'Object', 'Number', 'Class', 'StringBuilder', 'CharSequence', 'Comparable', 'Iterable', 'Enum',
115
+ ]);
46
116
  /** Bumped when the shape of a cached per-file record changes. */
47
- const CACHE_VERSION = 'nav-1';
117
+ const CACHE_VERSION = 'nav-3';
48
118
  const fileCache = new Map();
49
119
  export function buildNavigationIndex(files, options = {}) {
50
120
  return new NavigationIndex(files, options);
@@ -62,6 +132,10 @@ export class NavigationIndex {
62
132
  parseMs;
63
133
  /** Direct subtypes, by supertype FQN. */
64
134
  subtypes = new Map();
135
+ /** A call's binding, once made. The index is immutable, so it never goes stale. */
136
+ bindings = new Map();
137
+ /** Calls by `line:column`, per file, built on first use. */
138
+ callsAt = new Map();
65
139
  constructor(input, options) {
66
140
  const t0 = now();
67
141
  // A file in a language this index cannot resolve is **not searched**, and
@@ -132,6 +206,10 @@ export class NavigationIndex {
132
206
  * reported 147 callers of one method where the right answer was 0 `exact`,
133
207
  * 69 unverified and 78 contradicted by the resolver's own receiver type. Here
134
208
  * the 78 do not appear as answers at all, and the 69 arrive labelled.
209
+ *
210
+ * An overloaded method is answered per overload: with a parameter list in
211
+ * the symbol, for that overload alone; without one, for every overload,
212
+ * grouped and labelled, with a `note` saying so.
135
213
  */
136
214
  resolveCallers(query, opts = {}) {
137
215
  const t0 = now();
@@ -141,26 +219,107 @@ export class NavigationIndex {
141
219
  if (symbol === undefined) {
142
220
  return this.finish('callers', query, answers, unresolved, t0, opts);
143
221
  }
144
- const wantedName = lastSegment(symbol);
222
+ const q = parseSymbol(symbol);
223
+ const target = this.typesByFqn.get(q.owner);
224
+ const declared = target ? this.declaredOverloads(target, q.name) : [];
225
+ const wanted = q.params ? declared.filter(o => this.signatureMatches(o, q.params, target)) : declared;
226
+ const overloaded = declared.length > 1;
227
+ const extra = {};
228
+ if (q.params && target && declared.length > 0 && wanted.length === 0) {
229
+ extra.note =
230
+ `no overload of ${q.base} has the parameter list (${q.params.join(',')}); ` +
231
+ `declared: ${declared.map(o => `${q.base}${signatureOf(o)}`).join(', ')}`;
232
+ return this.finish('callers', query, answers, unresolved, t0, opts, extra);
233
+ }
234
+ const wantedSet = new Set(wanted);
235
+ const dispatchOpen = target !== undefined && q.name !== '<init>' && wanted.some(o => this.dispatchIsOpen(target, o));
145
236
  for (const rec of this.files.values()) {
146
237
  for (const call of rec.calls) {
147
- if (!this.nameCouldMatch(call, wantedName, symbol, rec))
238
+ if (!this.nameCouldMatch(call, q.name, q.owner))
148
239
  continue;
149
- const bound = this.bind(call, rec);
150
- if (bound && bound.target === symbol) {
151
- answers.push({ ...bound, methodName: call.method_name, site: this.siteOf(call, rec) });
152
- }
153
- else if (!bound) {
240
+ const b = this.bindCall(call, rec);
241
+ if (!b) {
154
242
  unresolved.push({
155
243
  site: this.siteOf(call, rec),
156
244
  reason: this.reasonFor(call, rec),
157
245
  methodName: call.method_name,
158
246
  });
247
+ continue;
248
+ }
249
+ if (!target)
250
+ continue;
251
+ if (b.kind === 'ambiguous') {
252
+ // Ambiguous among overloads none of which is the target is not this
253
+ // query's business; among overloads that include it, it is the
254
+ // honest half of the answer.
255
+ const relevant = b.candidates.some(c => (c.owner === target && wantedSet.has(c.overload)) || this.dispatchRelation(c.owner, c.overload, b.receiver, target, wanted) !== undefined);
256
+ if (relevant) {
257
+ unresolved.push({
258
+ site: this.siteOf(call, rec),
259
+ reason: 'overload-ambiguous',
260
+ methodName: call.method_name,
261
+ candidates: b.candidates.map(c => `${memberName(c.owner, c.overload)}${signatureOf(c.overload)}`),
262
+ });
263
+ }
264
+ continue;
265
+ }
266
+ if (b.owner === target && b.name === q.name) {
267
+ if (!b.overload) {
268
+ // A constructor call that fits none of the declared constructors,
269
+ // or a type that declares none: the type is certain, the overload
270
+ // is not, so a query about one overload cannot count it.
271
+ if (q.params && declared.length > 0) {
272
+ unresolved.push({
273
+ site: this.siteOf(call, rec),
274
+ reason: 'overload-ambiguous',
275
+ methodName: call.method_name,
276
+ candidates: declared.map(o => `${q.base}${signatureOf(o)}`),
277
+ });
278
+ }
279
+ else {
280
+ answers.push({ ...b.result, methodName: call.method_name, site: this.siteOf(call, rec) });
281
+ }
282
+ continue;
283
+ }
284
+ if (!wantedSet.has(b.overload))
285
+ continue; // another overload: answered elsewhere
286
+ answers.push({
287
+ ...b.result,
288
+ ...(overloaded ? { signature: signatureOf(b.overload) } : {}),
289
+ methodName: call.method_name,
290
+ site: this.siteOf(call, rec),
291
+ });
292
+ continue;
159
293
  }
160
- // A site bound to a different target is neither: it is answered, elsewhere.
294
+ if (!dispatchOpen || !b.overload)
295
+ continue;
296
+ const rel = this.dispatchRelation(b.owner, b.overload, b.receiver, target, wanted);
297
+ if (!rel)
298
+ continue;
299
+ const via = `${memberName(b.owner, b.overload)}`;
300
+ answers.push({
301
+ target: q.base,
302
+ tier: 'polymorphic',
303
+ ...(b.result.candidates ? { candidates: b.result.candidates } : {}),
304
+ ...(overloaded ? { signature: signatureOf(rel.overload) } : {}),
305
+ via: { member: via, relation: rel.relation },
306
+ evidence: rel.relation === 'supertype'
307
+ ? `the site calls ${via}, declared on supertype ${b.owner.fqn}; ${q.base} overrides it, so the call lands on it when the receiver is a ${target.fqn}`
308
+ : `the site calls ${via}, which overrides ${q.base}; it reaches ${q.base}'s body only through super, and is a caller of the operation ${q.base} declares`,
309
+ methodName: call.method_name,
310
+ site: this.siteOf(call, rec),
311
+ });
161
312
  }
162
313
  }
163
- return this.finish('callers', query, answers, unresolved, t0, opts);
314
+ if (overloaded && !q.params) {
315
+ extra.overloads = declared.map(o => ({
316
+ signature: signatureOf(o),
317
+ answers: answers.filter(a => a.signature === signatureOf(o)).length,
318
+ }));
319
+ extra.note = `${declared.length} overloads; pass a signature to narrow, e.g. ${q.base}${signatureOf(declared[0])}`;
320
+ }
321
+ const scope = dispatchOpen ? this.dispatchScope(q.base) : undefined;
322
+ return this.finish('callers', query, answers, unresolved, t0, opts, extra, scope);
164
323
  }
165
324
  /** Every call made from inside `symbol`'s body, bound the same way. */
166
325
  resolveCallees(query, opts = {}) {
@@ -171,9 +330,11 @@ export class NavigationIndex {
171
330
  if (symbol === undefined) {
172
331
  return this.finish('callees', query, answers, unresolved, t0, opts);
173
332
  }
174
- const owner = this.typesByFqn.get(stripLast(symbol));
175
- const method = owner?.methods.get(lastSegment(symbol));
176
- if (!owner || !method) {
333
+ const q = parseSymbol(symbol);
334
+ const owner = this.typesByFqn.get(q.owner);
335
+ const declared = owner ? this.declaredOverloads(owner, q.name) : [];
336
+ const bodies = q.params ? declared.filter(o => this.signatureMatches(o, q.params, owner)) : declared;
337
+ if (!owner || bodies.length === 0) {
177
338
  return this.finish('callees', query, answers, unresolved, t0, opts);
178
339
  }
179
340
  const rec = this.files.get(owner.file);
@@ -181,11 +342,25 @@ export class NavigationIndex {
181
342
  return this.finish('callees', query, answers, unresolved, t0, opts);
182
343
  for (const call of rec.calls) {
183
344
  const line = call.location.line;
184
- if (line < method.startLine || line > method.endLine)
345
+ if (!bodies.some(o => line >= o.startLine && line <= o.endLine))
185
346
  continue;
186
- const bound = this.bind(call, rec);
187
- if (bound)
188
- answers.push({ ...bound, methodName: call.method_name, site: this.siteOf(call, rec) });
347
+ const b = this.bindCall(call, rec);
348
+ if (b?.kind === 'bound') {
349
+ answers.push({
350
+ ...b.result,
351
+ ...(b.overload && this.declaredOverloads(b.owner, b.name).length > 1 ? { signature: signatureOf(b.overload) } : {}),
352
+ methodName: call.method_name,
353
+ site: this.siteOf(call, rec),
354
+ });
355
+ }
356
+ else if (b?.kind === 'ambiguous') {
357
+ unresolved.push({
358
+ site: this.siteOf(call, rec),
359
+ reason: 'overload-ambiguous',
360
+ methodName: call.method_name,
361
+ candidates: b.candidates.map(c => `${memberName(c.owner, c.overload)}${signatureOf(c.overload)}`),
362
+ });
363
+ }
189
364
  else {
190
365
  unresolved.push({
191
366
  site: this.siteOf(call, rec),
@@ -206,42 +381,59 @@ export class NavigationIndex {
206
381
  *
207
382
  * For `callers` the symbol is the call's own target — "who else calls what
208
383
  * this line calls". For `callees` it is the method whose body holds the
209
- * line — "what does the method I am looking at call". Returns nothing when
210
- * the site names no call, when the call cannot be bound to a target, or
211
- * when the name is written twice on one line, which the key cannot separate.
384
+ * line — "what does the method I am looking at call". Either carries a
385
+ * parameter list when the name is overloaded, so the follow-up query asks
386
+ * about that overload alone. Returns nothing when the site names no call,
387
+ * when the call cannot be bound to one target, or when the name is written
388
+ * twice on one line, which the key cannot separate.
212
389
  */
213
390
  symbolAt(file, line, methodName, kind) {
214
391
  const rec = this.files.get(file);
215
392
  if (!rec)
216
393
  return undefined;
217
- if (kind === 'callees') {
218
- const t = this.enclosingTypeAt(rec, line);
219
- if (!t)
220
- return undefined;
221
- for (const [name, m] of t.methods) {
222
- if (line >= m.startLine && line <= m.endLine)
223
- return `${t.fqn}.${name}`;
224
- }
225
- return undefined;
226
- }
394
+ if (kind === 'callees')
395
+ return this.methodAt(rec, line);
227
396
  const at = rec.calls.filter((c) => c.location.line === line && c.method_name === methodName);
228
397
  // The same name twice on one line is beyond this key, and picking one of
229
398
  // them would be a guess presented as an answer.
230
399
  if (at.length !== 1)
231
400
  return undefined;
232
- return this.bind(at[0], rec)?.target;
401
+ return this.symbolOfCall(at[0], rec);
233
402
  }
234
403
  // ------------------------------------------------------------ the binding
404
+ /** `bindCall`, reduced to what callers outside the query paths need. */
405
+ symbolOfCall(call, rec) {
406
+ const b = this.bindCall(call, rec);
407
+ if (b?.kind !== 'bound')
408
+ return undefined;
409
+ if (b.overload && this.declaredOverloads(b.owner, b.name).length > 1) {
410
+ return `${b.result.target}${signatureOf(b.overload)}`;
411
+ }
412
+ return b.result.target;
413
+ }
235
414
  /**
236
- * Bind one call site to a target, or to nothing.
415
+ * Bind one call site to a target, to several candidates, or to nothing.
237
416
  *
238
417
  * The order matters and is the whole contract: a receiver type is sought
239
418
  * first, and only when none can be had does a name-only bind happen, which
240
- * is labelled `inferred` and never anything better.
419
+ * is labelled `inferred` and never anything better. Within the type the
420
+ * receiver names, the overload is chosen by `selectOverload`, and a choice
421
+ * it cannot make is `ambiguous`, not a pick.
241
422
  */
242
- bind(call, rec) {
423
+ bindCall(call, rec, depth = 0) {
424
+ const memo = this.bindings.get(call);
425
+ if (memo !== undefined)
426
+ return memo ?? undefined;
427
+ const b = this.bindUncached(call, rec, depth);
428
+ // A bind made too deep inside another call's argument typing may have
429
+ // been cut short, so only a top-level result is remembered.
430
+ if (depth === 0)
431
+ this.bindings.set(call, b ?? null);
432
+ return b;
433
+ }
434
+ bindUncached(call, rec, depth) {
243
435
  // A constructor arrives as a call named after the type, with no receiver.
244
- const ctor = this.asConstructor(call, rec);
436
+ const ctor = this.bindConstructor(call, rec, depth);
245
437
  if (ctor)
246
438
  return ctor;
247
439
  const recv = this.receiverTypeOf(call, rec);
@@ -253,19 +445,25 @@ export class NavigationIndex {
253
445
  if (recv?.kind === 'foreign')
254
446
  return undefined;
255
447
  if (recv?.kind === 'project') {
256
- const hit = this.lookupThroughHierarchy(recv.fqn, call.method_name);
257
- if (hit)
258
- return this.tierFor(hit.owner, call.method_name, recv.evidence);
259
- // The receiver's type is known and does not have this method. Binding by
260
- // name here would be the contradiction the audit found 78 of, so no.
448
+ // The receiver's type is known; a method it does not have (at this
449
+ // arity) would be the contradiction the audit found 78 of, so no.
450
+ return this.bindInType(recv.fqn, call, rec, depth, recv.evidence, recv.fqn);
451
+ }
452
+ // `super.m()` names the superclass's member outright.
453
+ if (call.receiver === 'super') {
454
+ const encl = this.enclosingTypeOf(call, rec);
455
+ const sup = encl?.extendsName ? this.resolveTypeName(encl.extendsName, rec) : undefined;
456
+ if (sup && this.typesByFqn.has(sup)) {
457
+ return this.bindInType(sup, call, rec, depth, `super call inside ${encl.fqn}`, sup);
458
+ }
261
459
  return undefined;
262
460
  }
263
461
  // A statically imported member names its owner outright.
264
462
  const owner = this.staticOwnerFor(call, rec);
265
463
  if (owner) {
266
- const hit = this.lookupThroughHierarchy(owner.fqn, call.method_name);
267
- if (hit)
268
- return this.tierFor(hit.owner, call.method_name, owner.evidence);
464
+ const b = this.bindInType(owner.fqn, call, rec, depth, owner.evidence, undefined);
465
+ if (b)
466
+ return b;
269
467
  }
270
468
  // No receiver at all: an unqualified call is `this` or the enclosing type's
271
469
  // own static method. The enclosing type is known exactly, so this is not a
@@ -274,11 +472,8 @@ export class NavigationIndex {
274
472
  // lives in a subclass. Every path to a target now goes through `tierFor`.
275
473
  if (call.receiver === null && !call.receiver_type) {
276
474
  const encl = this.enclosingTypeOf(call, rec);
277
- if (encl) {
278
- const hit = this.lookupThroughHierarchy(encl.fqn, call.method_name);
279
- if (hit) {
280
- return this.tierFor(hit.owner, call.method_name, `unqualified call inside ${encl.fqn}`);
281
- }
475
+ if (encl && this.lookupThroughHierarchy(encl.fqn, call.method_name)) {
476
+ return this.bindInType(encl.fqn, call, rec, depth, `unqualified call inside ${encl.fqn}`, encl.fqn);
282
477
  }
283
478
  }
284
479
  // Nothing typed the receiver. A name-only bind is the floor, and only when
@@ -286,14 +481,49 @@ export class NavigationIndex {
286
481
  // not evidence of anything.
287
482
  const byName = this.uniqueByName(call.method_name);
288
483
  if (byName) {
484
+ const evidence = `bound by method name alone — no receiver type was available; ${call.method_name} is declared exactly once in the searched tree, on ${byName.fqn}`;
485
+ const cands = this.visibleOverloads(byName.fqn, call.method_name);
486
+ const sel = this.selectOverload(cands, call, rec, depth);
487
+ if (sel.kind === 'ambiguous') {
488
+ return { kind: 'ambiguous', owner: byName, name: call.method_name, candidates: sel.candidates, evidence };
489
+ }
490
+ if (sel.kind === 'none')
491
+ return undefined;
289
492
  return {
290
- target: `${byName.fqn}.${call.method_name}`,
291
- tier: 'inferred',
292
- evidence: `bound by method name alone — no receiver type was available; ${call.method_name} is declared exactly once in the searched tree, on ${byName.fqn}`,
493
+ kind: 'bound',
494
+ owner: sel.pick.owner,
495
+ name: call.method_name,
496
+ overload: sel.pick.overload,
497
+ result: { target: `${sel.pick.owner.fqn}.${call.method_name}`, tier: 'inferred', evidence },
293
498
  };
294
499
  }
295
500
  return undefined;
296
501
  }
502
+ /** Bind `call` to a member of `fqn` or of a supertype of it, choosing the overload. */
503
+ bindInType(fqn, call, rec, depth, evidence, receiver) {
504
+ const cands = this.visibleOverloads(fqn, call.method_name);
505
+ if (cands.length === 0)
506
+ return undefined;
507
+ const sel = this.selectOverload(cands, call, rec, depth);
508
+ if (sel.kind === 'none')
509
+ return undefined;
510
+ if (sel.kind === 'ambiguous') {
511
+ const owner = sel.candidates[0].owner;
512
+ return { kind: 'ambiguous', owner, name: call.method_name, candidates: sel.candidates, receiver, evidence };
513
+ }
514
+ const { owner, overload } = sel.pick;
515
+ const overloaded = this.declaredOverloads(owner, call.method_name).length > 1 || cands.length > 1;
516
+ return {
517
+ kind: 'bound',
518
+ owner,
519
+ name: call.method_name,
520
+ overload,
521
+ receiver,
522
+ result: overloaded
523
+ ? this.tierFor(owner, call.method_name, overload, `${evidence}; ${sel.how}`, true)
524
+ : this.tierFor(owner, call.method_name, overload, evidence),
525
+ };
526
+ }
297
527
  /**
298
528
  * The tier for a target, once the declaring type is known.
299
529
  *
@@ -306,9 +536,9 @@ export class NavigationIndex {
306
536
  * member named. The target stays the declaring member, which is what a
307
537
  * static resolver answers; only the tier changes.
308
538
  */
309
- tierFor(owner, method, evidence) {
310
- const impls = this.implementorsWith(owner.fqn, method);
311
- const bodyless = this.isBodyless(owner, method);
539
+ tierFor(owner, method, overload, evidence, overloaded = false) {
540
+ const impls = this.implementorsWith(owner.fqn, overload);
541
+ const bodyless = this.isBodyless(owner, overload);
312
542
  if (impls.length > 1 || bodyless) {
313
543
  return {
314
544
  target: `${owner.fqn}.${method}`,
@@ -322,11 +552,13 @@ export class NavigationIndex {
322
552
  return {
323
553
  target: `${owner.fqn}.${method}`,
324
554
  tier: 'exact',
325
- evidence: `${evidence}; ${method} is the unique method of that name on ${owner.fqn}, and it has a body`,
555
+ evidence: overloaded
556
+ ? `${evidence}; it is declared on ${owner.fqn}, and it has a body`
557
+ : `${evidence}; ${method} is the unique method of that name on ${owner.fqn}, and it has a body`,
326
558
  };
327
559
  }
328
- /** `new Widget()` → `app.Widget.<init>`. */
329
- asConstructor(call, rec) {
560
+ /** `new Widget()` → `app.Widget.<init>`, with the constructor chosen like any overload. */
561
+ bindConstructor(call, rec, depth) {
330
562
  const looksCtor = call.is_constructor === true ||
331
563
  (call.receiver === null &&
332
564
  !!call.receiver_type &&
@@ -337,12 +569,650 @@ export class NavigationIndex {
337
569
  const t = this.resolveTypeRecord(call.receiver_type ?? call.method_name, rec);
338
570
  if (!t)
339
571
  return undefined;
340
- return {
572
+ const result = {
341
573
  target: `${t.fqn}.<init>`,
342
574
  tier: 'exact',
343
575
  evidence: `object creation of ${t.fqn}, declared at ${t.file}:${t.startLine}`,
344
576
  };
577
+ const ctors = this.declaredOverloads(t, '<init>').map(overload => ({ owner: t, overload }));
578
+ // A type with no declared constructor has the implicit one; a call that
579
+ // fits none of the declared ones (a record's canonical constructor, say)
580
+ // still certainly creates this type. Either way the type is the answer
581
+ // and the overload is not known.
582
+ if (ctors.length === 0)
583
+ return { kind: 'bound', owner: t, name: '<init>', result };
584
+ const sel = this.selectOverload(ctors, call, rec, depth);
585
+ if (sel.kind === 'ambiguous') {
586
+ return { kind: 'ambiguous', owner: t, name: '<init>', candidates: sel.candidates, evidence: result.evidence ?? '' };
587
+ }
588
+ if (sel.kind === 'none')
589
+ return { kind: 'bound', owner: t, name: '<init>', result };
590
+ return {
591
+ kind: 'bound',
592
+ owner: t,
593
+ name: '<init>',
594
+ overload: sel.pick.overload,
595
+ result: ctors.length > 1 ? { ...result, evidence: `${result.evidence}; ${sel.how}` } : result,
596
+ };
345
597
  }
598
+ // ---------------------------------------------------- overload selection
599
+ /**
600
+ * The overloads of `name` a receiver of type `fqn` can see: its own, then
601
+ * its supertypes', nearest first, leaving out any a nearer type overrides.
602
+ */
603
+ visibleOverloads(fqn, name) {
604
+ const out = [];
605
+ const seen = new Set();
606
+ const queue = [fqn];
607
+ while (queue.length > 0) {
608
+ const f = queue.shift();
609
+ if (seen.has(f))
610
+ continue;
611
+ seen.add(f);
612
+ const t = this.typesByFqn.get(f);
613
+ if (!t)
614
+ continue;
615
+ for (const o of t.overloads.get(name) ?? []) {
616
+ if (o.isConstructor)
617
+ continue;
618
+ if (out.some(c => this.sameErasure(c, { owner: t, overload: o })))
619
+ continue;
620
+ out.push({ owner: t, overload: o });
621
+ }
622
+ const rec = this.files.get(t.file);
623
+ for (const sup of t.superNames) {
624
+ const supFqn = rec ? this.resolveTypeName(sup, rec) : undefined;
625
+ if (supFqn)
626
+ queue.push(supFqn);
627
+ }
628
+ }
629
+ return out;
630
+ }
631
+ /** The declarations of `name` on `t` itself. `<init>` is its constructors. */
632
+ declaredOverloads(t, name) {
633
+ if (name === '<init>')
634
+ return (t.overloads.get(t.simpleName) ?? []).filter(o => o.isConstructor);
635
+ return (t.overloads.get(name) ?? []).filter(o => !o.isConstructor);
636
+ }
637
+ /**
638
+ * Choose the overload a call invokes, the way the compiler does — or say
639
+ * that the types read off the source do not decide it.
640
+ *
641
+ * Arity first. Then the compiler's three phases: without boxing or
642
+ * varargs, with boxing, with varargs. In the first phase that has any
643
+ * applicable overload, the most specific one wins. Every argument type here
644
+ * is a lower bound on what is known — an argument that cannot be typed fits
645
+ * anything — so the rule is applied only where it is sound: an overload is
646
+ * picked when it is **certainly** applicable and more specific than every
647
+ * overload that **might** be. Anything less is `ambiguous`.
648
+ */
649
+ selectOverload(cands, call, rec, depth) {
650
+ const n = call.arguments.length;
651
+ const byArity = cands.filter(c => fitsArity(c.overload, n));
652
+ if (byArity.length === 0)
653
+ return { kind: 'none' };
654
+ const args = call.arguments.map(a => this.argumentType(a, call, rec, depth));
655
+ if (byArity.length === 1) {
656
+ // The only declaration in the tree at this arity, but the argument
657
+ // types certainly rule it out: what is called is an overload this
658
+ // index cannot see, inherited from a library type. Not ours.
659
+ if ([1, 2, 3].every(p => this.applicable(byArity[0], args, p) === 'no'))
660
+ return { kind: 'none' };
661
+ return {
662
+ kind: 'one',
663
+ pick: byArity[0],
664
+ how: `overload ${signatureOf(byArity[0].overload)} is the only one taking ${n} argument(s)`,
665
+ };
666
+ }
667
+ const fit = new Map();
668
+ for (const c of byArity)
669
+ fit.set(c, [1, 2, 3].map(p => this.applicable(c, args, p)));
670
+ const possible = byArity.filter(c => fit.get(c).some(f => f !== 'no'));
671
+ if (possible.length === 1) {
672
+ return {
673
+ kind: 'one',
674
+ pick: possible[0],
675
+ how: `overload ${signatureOf(possible[0].overload)} is the only one the argument types admit`,
676
+ };
677
+ }
678
+ // The argument types rule out every overload: something read off the
679
+ // source is wrong, so nothing is chosen.
680
+ if (possible.length === 0)
681
+ return { kind: 'ambiguous', candidates: byArity };
682
+ for (const phase of [1, 2, 3]) {
683
+ const P = possible.filter(c => fit.get(c)[phase - 1] !== 'no');
684
+ if (P.length === 0)
685
+ continue;
686
+ const C = P.filter(c => fit.get(c)[phase - 1] === 'yes');
687
+ if (C.length === 0)
688
+ return { kind: 'ambiguous', candidates: possible };
689
+ const best = C.find(m => P.every(o => o === m || this.moreSpecific(m, o, phase)));
690
+ if (best) {
691
+ return {
692
+ kind: 'one',
693
+ pick: best,
694
+ how: `overload ${signatureOf(best.overload)} is the most specific one the argument types admit`,
695
+ };
696
+ }
697
+ return { kind: 'ambiguous', candidates: P };
698
+ }
699
+ return { kind: 'ambiguous', candidates: possible };
700
+ }
701
+ /** Whether `c` applies to arguments of these types in compiler phase 1, 2 or 3. */
702
+ applicable(c, args, phase) {
703
+ const o = c.overload;
704
+ const declRec = this.files.get(c.owner.file);
705
+ const params = o.params.map(p => this.typeRef(p, declRec));
706
+ let worst = 'yes';
707
+ const meet = (f) => {
708
+ if (f === 'no')
709
+ worst = 'no';
710
+ else if (f === 'maybe' && worst === 'yes')
711
+ worst = 'maybe';
712
+ };
713
+ if (phase < 3) {
714
+ if (args.length !== params.length)
715
+ return 'no';
716
+ args.forEach((a, i) => meet(this.fits(a, params[i], phase)));
717
+ return worst;
718
+ }
719
+ if (!o.varargs || args.length < params.length - 1)
720
+ return 'no';
721
+ const last = params[params.length - 1];
722
+ const elem = { ...last, dims: Math.max(0, last.dims - 1) };
723
+ args.forEach((a, i) => meet(this.fits(a, i < params.length - 1 ? params[i] : elem, 2)));
724
+ return worst;
725
+ }
726
+ /** `m` is more specific than `o`: each of its parameter types fits the other's. */
727
+ moreSpecific(m, o, phase) {
728
+ if (phase === 3)
729
+ return false;
730
+ const mp = m.overload.params.map(p => this.typeRef(p, this.files.get(m.owner.file)));
731
+ const op = o.overload.params.map(p => this.typeRef(p, this.files.get(o.owner.file)));
732
+ if (mp.length !== op.length)
733
+ return false;
734
+ return mp.every((t, i) => this.fits(t, op[i], 1) === 'yes');
735
+ }
736
+ /**
737
+ * Whether a value of type `a` can be passed as `p` — certainly, possibly,
738
+ * or certainly not. `a === null` is an argument that could not be typed.
739
+ */
740
+ fits(a, p, phase) {
741
+ if (!a)
742
+ return 'maybe';
743
+ if (p.typeVar)
744
+ return a.prim && a.dims === 0 && phase === 1 ? 'no' : 'maybe';
745
+ if (a.simple === 'null' && a.dims === 0)
746
+ return p.prim && p.dims === 0 ? 'no' : 'yes';
747
+ if (a.typeVar)
748
+ return 'maybe';
749
+ if (a.dims > 0 || p.dims > 0) {
750
+ if (a.dims === p.dims) {
751
+ const ae = { ...a, dims: 0 };
752
+ const pe = { ...p, dims: 0 };
753
+ if (ae.prim || pe.prim)
754
+ return ae.prim && pe.prim && ae.simple === pe.simple ? 'yes' : 'no';
755
+ return this.fits(ae, pe, 1);
756
+ }
757
+ if (a.dims > 0 && p.dims === 0) {
758
+ if (['Object', 'Cloneable', 'Serializable'].includes(p.simple))
759
+ return 'yes';
760
+ return p.prim || this.isProjectRef(p) || p.simple in JDK_SUPERS ? 'no' : 'maybe';
761
+ }
762
+ if (a.dims > p.dims && p.simple === 'Object' && !p.prim)
763
+ return 'yes';
764
+ return 'no';
765
+ }
766
+ if (a.prim && p.prim)
767
+ return WIDENS_TO[a.simple]?.includes(p.simple) ? 'yes' : 'no';
768
+ if (a.prim) {
769
+ if (phase === 1)
770
+ return 'no';
771
+ const boxed = BOXES[a.simple];
772
+ return boxed ? this.refFits({ simple: boxed, fqn: `java.lang.${boxed}`, dims: 0, prim: false, typeVar: false }, p) : 'no';
773
+ }
774
+ if (p.prim) {
775
+ if (phase === 1)
776
+ return 'no';
777
+ const unboxed = UNBOXES[a.simple];
778
+ if (unboxed && (!a.fqn || a.fqn === `java.lang.${a.simple}`))
779
+ return WIDENS_TO[unboxed].includes(p.simple) ? 'yes' : 'no';
780
+ return this.isProjectRef(a) || a.simple in JDK_SUPERS ? 'no' : 'maybe';
781
+ }
782
+ return this.refFits(a, p);
783
+ }
784
+ /** Reference to reference: subtyping, as far as the tree and the JDK table know it. */
785
+ refFits(a, p) {
786
+ if (a.fqn && p.fqn && a.fqn === p.fqn)
787
+ return 'yes';
788
+ if (p.simple === 'Object' && (!p.fqn || p.fqn === 'java.lang.Object'))
789
+ return 'yes';
790
+ const aProject = this.isProjectRef(a);
791
+ const pProject = this.isProjectRef(p);
792
+ if (aProject) {
793
+ const sup = this.supertypesOf(a.fqn);
794
+ if (pProject) {
795
+ if (sup.project.has(p.fqn))
796
+ return 'yes';
797
+ // A library type never extends one of ours, so only a supertype name
798
+ // this index could not resolve keeps the door open.
799
+ return sup.unresolved.length > 0 ? 'maybe' : 'no';
800
+ }
801
+ if (sup.foreign.includes(p.simple))
802
+ return 'yes';
803
+ // Every supertype is in the tree or a JDK type whose own supertypes are
804
+ // known, and none of them is `p`: it cannot be one.
805
+ const closed = sup.unresolved.length === 0 && sup.foreign.every(f => LEAF_INTERFACES.has(f) || (JDK_SUPERS[f] && !JDK_SUPERS[f].supers.includes(p.simple)));
806
+ if (closed && this.isKnownForeignRef(p))
807
+ return 'no';
808
+ const jdk = JDK_SUPERS[p.simple];
809
+ if (jdk) {
810
+ // A JDK class (not interface) is reachable only through a superclass
811
+ // chain that leaves the tree.
812
+ return sup.foreignClassChain ? 'maybe' : 'no';
813
+ }
814
+ return 'maybe';
815
+ }
816
+ if (pProject) {
817
+ // A library type cannot be a subtype of a type in this tree. Only an
818
+ // argument whose type could not be placed at all might still be ours.
819
+ return a.fqn || a.simple in JDK_SUPERS ? 'no' : 'maybe';
820
+ }
821
+ if (a.simple === p.simple)
822
+ return a.fqn && p.fqn && a.fqn !== p.fqn ? 'no' : 'yes';
823
+ const ja = JDK_SUPERS[a.simple];
824
+ if (ja) {
825
+ if (ja.supers.includes(p.simple))
826
+ return 'yes';
827
+ if (p.simple in JDK_SUPERS)
828
+ return 'no';
829
+ return ja.final ? 'no' : 'maybe';
830
+ }
831
+ const jp = JDK_SUPERS[p.simple];
832
+ if (jp?.final)
833
+ return 'no';
834
+ return 'maybe';
835
+ }
836
+ /** Is `t` certainly a type outside this tree? */
837
+ isKnownForeignRef(t) {
838
+ if (t.fqn)
839
+ return !this.isProjectFqn(t.fqn) && !this.typesByFqn.has(t.fqn);
840
+ return JAVA_LANG.has(t.simple) || t.simple in JDK_SUPERS;
841
+ }
842
+ /** Is `t` a type declared in this tree? */
843
+ isProjectRef(t) {
844
+ return !!t.fqn && this.typesByFqn.has(t.fqn);
845
+ }
846
+ /** Every supertype of a project type, split into what the tree holds and what it does not. */
847
+ supertypesOf(fqn) {
848
+ const project = new Set();
849
+ const foreign = [];
850
+ const unresolved = [];
851
+ let foreignClassChain = false;
852
+ const walk = (f, viaClass) => {
853
+ if (project.has(f))
854
+ return;
855
+ project.add(f);
856
+ const t = this.typesByFqn.get(f);
857
+ const rec = t ? this.files.get(t.file) : undefined;
858
+ if (!t || !rec)
859
+ return;
860
+ for (const sup of t.superNames) {
861
+ const resolved = this.resolveTypeName(sup, rec);
862
+ const isClassEdge = viaClass && sup === t.extendsName;
863
+ if (resolved && this.typesByFqn.has(resolved)) {
864
+ walk(resolved, isClassEdge);
865
+ continue;
866
+ }
867
+ const simple = eraseType(sup).split('.').pop() ?? sup;
868
+ if (this.isKnownForeign(sup, rec))
869
+ foreign.push(simple);
870
+ else
871
+ unresolved.push(simple);
872
+ if (isClassEdge)
873
+ foreignClassChain = true;
874
+ }
875
+ };
876
+ walk(fqn, true);
877
+ return { project, foreign, unresolved, foreignClassChain };
878
+ }
879
+ /** A type name that certainly names something outside the tree. */
880
+ isKnownForeign(name, rec) {
881
+ const bare = eraseType(name);
882
+ const simple = bare.split('.').pop() ?? bare;
883
+ if (bare.includes('.'))
884
+ return !this.isProjectFqn(bare);
885
+ const imported = rec.imports.get(simple);
886
+ if (imported)
887
+ return !this.isProjectFqn(imported);
888
+ return JAVA_LANG.has(simple) || simple in JDK_SUPERS;
889
+ }
890
+ /**
891
+ * The static type of one argument, or `null` when it cannot be known. The
892
+ * extractor types what the syntax and the declarations in scope say; an
893
+ * argument that is itself a call is typed here by binding that call and
894
+ * reading the chosen overload's return type.
895
+ */
896
+ argumentType(arg, call, rec, depth) {
897
+ if (arg.type)
898
+ return this.typeRef(arg.type, rec);
899
+ if (!arg.location || depth >= 4)
900
+ return null;
901
+ const name = outerCallName(arg.expression);
902
+ if (!name)
903
+ return null;
904
+ const inner = this.callAt(rec, arg.location.line, arg.location.column, name);
905
+ if (!inner || inner === call)
906
+ return null;
907
+ const b = this.bindCall(inner, rec, depth + 1);
908
+ let returned;
909
+ let receiver;
910
+ if (b?.kind === 'bound' && b.overload?.returnType) {
911
+ returned = { text: b.overload.returnType, owner: b.owner };
912
+ receiver = b.receiver;
913
+ }
914
+ else if (!b) {
915
+ // An accessor that annotation processing writes: what it returns is the
916
+ // field it reads.
917
+ const recv = this.receiverTypeOf(inner, rec);
918
+ receiver = recv?.kind === 'project' ? recv.fqn : inner.receiver === null ? this.enclosingTypeOf(inner, rec)?.fqn : undefined;
919
+ if (receiver && inner.arguments.length === 0)
920
+ returned = this.generatedGetterType(receiver, inner.method_name);
921
+ }
922
+ if (!returned)
923
+ return null;
924
+ const sub = this.substitute(returned.text, returned.owner, receiver);
925
+ if (!sub)
926
+ return null;
927
+ const t = this.typeRef(sub.text, sub.rec);
928
+ return t.typeVar ? null : t;
929
+ }
930
+ /**
931
+ * The type a generated `getX()` / `isX()` returns on `fqn`: the written
932
+ * type of field `x`, with the type that declares the field.
933
+ */
934
+ generatedGetterType(fqn, method) {
935
+ const m = /^(get|is)([A-Z]\w*)$/.exec(method);
936
+ const t = this.typesByFqn.get(fqn);
937
+ if (!m || !t || !this.wouldBeGenerated(t, method))
938
+ return undefined;
939
+ const field = m[2].charAt(0).toLowerCase() + m[2].slice(1);
940
+ const seen = new Set();
941
+ const queue = [fqn];
942
+ while (queue.length > 0) {
943
+ const f = queue.shift();
944
+ if (seen.has(f))
945
+ continue;
946
+ seen.add(f);
947
+ const ft = this.typesByFqn.get(f);
948
+ const frec = ft ? this.files.get(ft.file) : undefined;
949
+ if (!ft || !frec)
950
+ continue;
951
+ const type = ft.fieldTypes.get(field);
952
+ if (type)
953
+ return { text: type, owner: ft };
954
+ for (const sup of ft.superNames) {
955
+ const s = this.resolveTypeName(sup, frec);
956
+ if (s)
957
+ queue.push(s);
958
+ }
959
+ }
960
+ return undefined;
961
+ }
962
+ /**
963
+ * A member's written type as seen from a receiver of type `from`: a type
964
+ * parameter of the declaring type is replaced by the argument the
965
+ * superclass chain from `from` up to it supplies — `Loan extends
966
+ * Base<Long>` makes `Base<T>.getId()`'s `T` a `Long`. Returns the text and
967
+ * the file it must be resolved in, or nothing when a type parameter is
968
+ * left unbound.
969
+ */
970
+ substitute(written, owner, from) {
971
+ const ownRec = this.files.get(owner.file);
972
+ const bare = eraseType(written).replace(/(\[\])+$/, '');
973
+ if (!owner.typeParams.includes(bare))
974
+ return { text: written, rec: ownRec };
975
+ if (!from || from === owner.fqn)
976
+ return undefined;
977
+ const queue = [{ fqn: from, env: new Map() }];
978
+ const seen = new Set();
979
+ while (queue.length > 0) {
980
+ const { fqn, env } = queue.shift();
981
+ if (fqn === owner.fqn) {
982
+ const bound = env.get(bare);
983
+ if (!bound)
984
+ return undefined;
985
+ const dims = written.slice(written.lastIndexOf(bare) + bare.length).match(/\[\]/g)?.length ?? 0;
986
+ return { text: `${bound.text}${'[]'.repeat(dims)}`, rec: bound.rec };
987
+ }
988
+ if (seen.has(fqn))
989
+ continue;
990
+ seen.add(fqn);
991
+ const t = this.typesByFqn.get(fqn);
992
+ const rec = t ? this.files.get(t.file) : undefined;
993
+ if (!t || !rec)
994
+ continue;
995
+ for (const sup of t.superNames) {
996
+ const supFqn = this.resolveTypeName(sup, rec);
997
+ const st = supFqn ? this.typesByFqn.get(supFqn) : undefined;
998
+ if (!st)
999
+ continue;
1000
+ const open = sup.indexOf('<');
1001
+ const args = open === -1 ? [] : splitTopLevel(sup.slice(open + 1, sup.lastIndexOf('>'))).map(a => a.trim());
1002
+ const next = new Map();
1003
+ st.typeParams.forEach((p, i) => {
1004
+ const a = args[i];
1005
+ if (!a)
1006
+ return;
1007
+ next.set(p, env.get(a) ?? { text: a, rec });
1008
+ });
1009
+ queue.push({ fqn: st.fqn, env: next });
1010
+ }
1011
+ }
1012
+ return undefined;
1013
+ }
1014
+ /** The outermost call named `name` that starts at `line:column` in `rec`. */
1015
+ callAt(rec, line, column, name) {
1016
+ let byPos = this.callsAt.get(rec.path);
1017
+ if (!byPos) {
1018
+ byPos = new Map();
1019
+ for (const c of rec.calls)
1020
+ push(byPos, `${c.location.line}:${c.location.column}`, c);
1021
+ this.callsAt.set(rec.path, byPos);
1022
+ }
1023
+ const here = (byPos.get(`${line}:${column}`) ?? []).filter(c => c.method_name === name);
1024
+ // `a.x().x()` puts two calls named `x` at one position; the outer one has
1025
+ // the longer receiver.
1026
+ return here.sort((x, y) => (y.receiver?.length ?? 0) - (x.receiver?.length ?? 0))[0];
1027
+ }
1028
+ /** A written type, resolved from one file's point of view. */
1029
+ typeRef(written, rec) {
1030
+ let t = written.replace(/@[\w.]+(\([^)]*\))?\s*/g, '').replace(/\bfinal\s+/g, '').trim();
1031
+ let dims = 0;
1032
+ if (t.endsWith('...')) {
1033
+ dims++;
1034
+ t = t.slice(0, -3).trim();
1035
+ }
1036
+ t = eraseType(t);
1037
+ while (t.endsWith('[]')) {
1038
+ dims++;
1039
+ t = t.slice(0, -2).trim();
1040
+ }
1041
+ const simple = t.split('.').pop() ?? t;
1042
+ if (PRIMITIVES.has(t))
1043
+ return { simple: t, fqn: t, dims, prim: true, typeVar: false };
1044
+ if (t === 'null')
1045
+ return { simple: 'null', fqn: null, dims: 0, prim: false, typeVar: false };
1046
+ let fqn = null;
1047
+ if (t.includes('.')) {
1048
+ fqn = (rec ? this.resolveTypeName(t, rec) : undefined) ?? t;
1049
+ }
1050
+ else if (rec) {
1051
+ // An import names the type outright, ours or not. Failing that, a JDK
1052
+ // name is the JDK's unless this package declares its own — the tree-wide
1053
+ // unique-simple-name fallback must not turn `Date` into a project type.
1054
+ const imported = rec.imports.get(t);
1055
+ if (imported)
1056
+ fqn = imported;
1057
+ else if ((JAVA_LANG.has(t) || t in JDK_SUPERS) && !this.typesByFqn.has(rec.pkg ? `${rec.pkg}.${t}` : t))
1058
+ fqn = null;
1059
+ else
1060
+ fqn = this.resolveTypeName(t, rec) ?? null;
1061
+ }
1062
+ if (!fqn && JAVA_LANG.has(t))
1063
+ fqn = `java.lang.${t}`;
1064
+ const typeVar = !fqn && /^[A-Z][A-Z0-9]?$/.test(t);
1065
+ return { simple, fqn, dims, prim: false, typeVar };
1066
+ }
1067
+ /** Whether overload `o` of `owner` is what a query's parameter list names. */
1068
+ signatureMatches(o, params, owner) {
1069
+ if (o.params.length !== params.length)
1070
+ return false;
1071
+ const rec = this.files.get(owner.file);
1072
+ return o.params.every((p, i) => {
1073
+ const want = this.typeRef(params[i], undefined);
1074
+ const have = this.typeRef(p, rec);
1075
+ if (want.dims !== have.dims)
1076
+ return false;
1077
+ const wantBare = eraseType(params[i].replace(/\.\.\.$/, '').replace(/(\[\])+$/, ''));
1078
+ if (wantBare.includes('.')) {
1079
+ if (have.fqn && !have.prim)
1080
+ return have.fqn === wantBare;
1081
+ return have.simple === want.simple;
1082
+ }
1083
+ return have.simple === want.simple;
1084
+ });
1085
+ }
1086
+ /** Same name and same erased parameter types: one overrides the other. */
1087
+ sameErasure(a, b) {
1088
+ const pa = a.overload.params;
1089
+ const pb = b.overload.params;
1090
+ if (a.overload.name !== b.overload.name || pa.length !== pb.length)
1091
+ return false;
1092
+ const ra = this.files.get(a.owner.file);
1093
+ const rb = this.files.get(b.owner.file);
1094
+ return pa.every((p, i) => {
1095
+ const x = this.typeRef(p, ra);
1096
+ const y = this.typeRef(pb[i], rb);
1097
+ if (x.typeVar || y.typeVar)
1098
+ return x.dims === y.dims;
1099
+ return x.simple === y.simple && x.dims === y.dims;
1100
+ });
1101
+ }
1102
+ // --------------------------------------------------------------- dispatch
1103
+ /**
1104
+ * How a site bound to `owner.overload` reaches `target`'s overload, if it
1105
+ * does: through a supertype's declaration the target overrides, or by
1106
+ * calling an override of the target.
1107
+ */
1108
+ dispatchRelation(owner, overload, receiver, target, wanted) {
1109
+ if (owner === target || overload.isConstructor || overload.modifiers.includes('static'))
1110
+ return undefined;
1111
+ for (const w of wanted) {
1112
+ if (w.modifiers.includes('static') || w.modifiers.includes('private'))
1113
+ continue;
1114
+ if (!this.sameErasure({ owner, overload }, { owner: target, overload: w }))
1115
+ continue;
1116
+ if (this.isSubtype(target.fqn, owner.fqn)) {
1117
+ // The receiver's static type must admit the target's type, or the
1118
+ // object can never be one: a sibling's call does not land here.
1119
+ if (!receiver || this.isSubtype(target.fqn, receiver))
1120
+ return { relation: 'supertype', overload: w };
1121
+ }
1122
+ else if (this.isSubtype(owner.fqn, target.fqn)) {
1123
+ return { relation: 'override', overload: w };
1124
+ }
1125
+ }
1126
+ return undefined;
1127
+ }
1128
+ /** Whether calls to `o` on `t` can dispatch anywhere but `t`'s own body. */
1129
+ dispatchIsOpen(t, o) {
1130
+ if (o.isConstructor || o.modifiers.includes('static') || o.modifiers.includes('private'))
1131
+ return false;
1132
+ if (this.isBodyless(t, o))
1133
+ return true;
1134
+ const self = { owner: t, overload: o };
1135
+ // Not `visibleOverloads`: it leaves out exactly the declarations an
1136
+ // override hides, which are the ones that make dispatch open.
1137
+ const declaresAbove = (f, seen) => {
1138
+ if (seen.has(f))
1139
+ return false;
1140
+ seen.add(f);
1141
+ const st = this.typesByFqn.get(f);
1142
+ const rec = st ? this.files.get(st.file) : undefined;
1143
+ if (!st || !rec)
1144
+ return false;
1145
+ if (st !== t && (st.overloads.get(o.name) ?? []).some(x => this.sameErasure({ owner: st, overload: x }, self)))
1146
+ return true;
1147
+ return st.superNames.some(sup => {
1148
+ const s = this.resolveTypeName(sup, rec);
1149
+ return s !== undefined && declaresAbove(s, seen);
1150
+ });
1151
+ };
1152
+ if (declaresAbove(t.fqn, new Set()))
1153
+ return true;
1154
+ const seen = new Set();
1155
+ const below = (f) => {
1156
+ for (const sub of this.subtypes.get(f) ?? []) {
1157
+ if (seen.has(sub))
1158
+ continue;
1159
+ seen.add(sub);
1160
+ const st = this.typesByFqn.get(sub);
1161
+ if (st && (st.overloads.get(o.name) ?? []).some(x => this.sameErasure({ owner: st, overload: x }, self)))
1162
+ return true;
1163
+ if (below(sub))
1164
+ return true;
1165
+ }
1166
+ return false;
1167
+ };
1168
+ return below(t.fqn);
1169
+ }
1170
+ /** `a` is `b` or a subtype of it, by the edges the tree holds. */
1171
+ isSubtype(a, b) {
1172
+ if (a === b)
1173
+ return true;
1174
+ const seen = new Set();
1175
+ const walk = (f) => {
1176
+ if (seen.has(f))
1177
+ return false;
1178
+ seen.add(f);
1179
+ const t = this.typesByFqn.get(f);
1180
+ const rec = t ? this.files.get(t.file) : undefined;
1181
+ if (!t || !rec)
1182
+ return false;
1183
+ for (const sup of t.superNames) {
1184
+ const s = this.resolveTypeName(sup, rec);
1185
+ if (!s)
1186
+ continue;
1187
+ if (s === b || walk(s))
1188
+ return true;
1189
+ }
1190
+ return false;
1191
+ };
1192
+ return walk(a);
1193
+ }
1194
+ /** The query's scope, with what dispatch it cannot see said out loud. */
1195
+ dispatchScope(symbol) {
1196
+ return {
1197
+ searched: this.scope.searched,
1198
+ excluded: [
1199
+ ...this.scope.excluded,
1200
+ {
1201
+ pattern: 'subtypes outside the searched tree',
1202
+ reason: `dispatch: an override of ${symbol}, or a call through a supertype, in a library or module not indexed here is not seen`,
1203
+ },
1204
+ {
1205
+ pattern: 'reflection',
1206
+ reason: 'dynamic: Method.invoke, proxies and method handles are not followed',
1207
+ },
1208
+ {
1209
+ pattern: 'framework dispatch',
1210
+ reason: 'a framework that invokes a method by annotation, configuration or naming convention leaves no call site to answer',
1211
+ },
1212
+ ],
1213
+ };
1214
+ }
1215
+ // ---------------------------------------------------------- receiver type
346
1216
  /**
347
1217
  * The receiver's static type, where it can be had without inventing one.
348
1218
  *
@@ -392,7 +1262,8 @@ export class NavigationIndex {
392
1262
  * `Builder.of().step("a")` is typed by resolving `Builder`, then taking
393
1263
  * `of`'s declared return type, then `step`'s. A step whose return type is
394
1264
  * unknown ends the walk and the whole expression is untyped — a half-walked
395
- * chain is not a type.
1265
+ * chain is not a type. So does a step whose overloads of that arity
1266
+ * disagree on what they return.
396
1267
  */
397
1268
  typeOfExpression(expr, rec, depth) {
398
1269
  if (!expr || depth > 8)
@@ -425,27 +1296,37 @@ export class NavigationIndex {
425
1296
  rec.staticWildcards.find(w => this.typesByFqn.get(w)?.methods.has(root.name)) ??
426
1297
  rec.types.find(t => t.methods.has(root.name))?.fqn;
427
1298
  const owner = ownerFqn ? this.typesByFqn.get(ownerFqn) : undefined;
428
- const m = owner?.methods.get(root.name);
429
- if (!owner || !m || !m.returnType)
1299
+ const returnType = owner ? this.returnTypeAt(owner.fqn, root.name, root.argc) : undefined;
1300
+ if (!owner || !returnType)
430
1301
  return undefined;
431
- current = this.resolveTypeRecord(m.returnType, rec);
1302
+ current = this.resolveTypeRecord(returnType, rec);
432
1303
  if (!current)
433
1304
  return undefined;
434
- evidence = `chain rooted at ${owner.fqn}.${root.name}, which returns ${m.returnType}`;
1305
+ evidence = `chain rooted at ${owner.fqn}.${root.name}, which returns ${returnType}`;
435
1306
  }
436
1307
  for (const step of chain.steps) {
437
- const hit = this.lookupThroughHierarchy(current.fqn, step.name);
438
- const m = hit?.owner.methods.get(step.name);
439
- if (!m || !m.returnType)
1308
+ const returnType = this.returnTypeAt(current.fqn, step.name, step.argc);
1309
+ if (!returnType)
440
1310
  return undefined;
441
- const next = this.resolveTypeRecord(m.returnType, rec);
1311
+ const next = this.resolveTypeRecord(returnType, rec);
442
1312
  if (!next)
443
1313
  return undefined;
444
- evidence += ` → ${step.name}() returns ${m.returnType}`;
1314
+ evidence += ` → ${step.name}() returns ${returnType}`;
445
1315
  current = next;
446
1316
  }
447
1317
  return { kind: 'project', fqn: current.fqn, evidence };
448
1318
  }
1319
+ /**
1320
+ * The return type of `name` called with `argc` arguments on `fqn`, when
1321
+ * every overload of that arity agrees on it.
1322
+ */
1323
+ returnTypeAt(fqn, name, argc) {
1324
+ const cands = this.visibleOverloads(fqn, name).filter(c => fitsArity(c.overload, argc));
1325
+ const types = new Set(cands.map(c => c.overload.returnType));
1326
+ if (types.size !== 1)
1327
+ return undefined;
1328
+ return [...types][0] ?? undefined;
1329
+ }
449
1330
  /** The owner named by a static import of this call's method. */
450
1331
  staticOwnerFor(call, rec) {
451
1332
  if (call.receiver !== null)
@@ -475,6 +1356,21 @@ export class NavigationIndex {
475
1356
  }
476
1357
  return best ?? rec.types[0];
477
1358
  }
1359
+ /** The method whose body holds `line`, with a parameter list when its name is overloaded. */
1360
+ methodAt(rec, line) {
1361
+ const t = this.enclosingTypeAt(rec, line);
1362
+ if (!t)
1363
+ return undefined;
1364
+ for (const [name, list] of t.overloads) {
1365
+ for (const o of list) {
1366
+ if (line < o.startLine || line > o.endLine)
1367
+ continue;
1368
+ const member = o.isConstructor ? `${t.fqn}.<init>` : `${t.fqn}.${name}`;
1369
+ return list.length > 1 ? `${member}${signatureOf(o)}` : member;
1370
+ }
1371
+ }
1372
+ return undefined;
1373
+ }
478
1374
  /** Find `method` on `fqn` or on a supertype of it, inside the tree. */
479
1375
  lookupThroughHierarchy(fqn, method, seen = new Set()) {
480
1376
  if (seen.has(fqn))
@@ -510,24 +1406,21 @@ export class NavigationIndex {
510
1406
  * That is the conservative direction: it costs an answer its `exact` label
511
1407
  * and never grants one.
512
1408
  */
513
- isBodyless(owner, method) {
514
- const m = owner.methods.get(method);
515
- if (!m)
516
- return false;
517
- if (m.modifiers.includes('abstract'))
1409
+ isBodyless(owner, o) {
1410
+ if (o.modifiers.includes('abstract'))
518
1411
  return true;
519
1412
  if (owner.kind !== 'interface')
520
1413
  return false;
521
- if (m.modifiers.includes('static') || m.modifiers.includes('default'))
1414
+ if (o.modifiers.includes('static') || o.modifiers.includes('default'))
522
1415
  return false;
523
- const line = this.files.get(owner.file)?.lines?.[m.startLine - 1];
1416
+ const line = this.files.get(owner.file)?.lines?.[o.startLine - 1];
524
1417
  if (line === undefined)
525
1418
  return true;
526
1419
  return !/\b(default|static)\b/.test(line);
527
1420
  }
528
1421
  /**
529
- * Types in the tree that declare a **runnable** `method` and are `fqn` or
530
- * below it.
1422
+ * Types in the tree that declare a **runnable** override of `o` and are
1423
+ * `fqn` or below it.
531
1424
  *
532
1425
  * "Runnable" is the whole of it: `candidates` answers "which body could
533
1426
  * run", so an abstract declaration does not belong there even when it is
@@ -535,15 +1428,19 @@ export class NavigationIndex {
535
1428
  * with nothing overriding `step` has no candidates, and saying so is the
536
1429
  * honest answer — listing `Base.step` would name a body that does not exist.
537
1430
  */
538
- implementorsWith(fqn, method) {
1431
+ implementorsWith(fqn, o) {
539
1432
  const out = [];
1433
+ const start = this.typesByFqn.get(fqn);
1434
+ const self = start ? { owner: start, overload: o } : undefined;
540
1435
  const walk = (f, seen) => {
541
1436
  if (seen.has(f))
542
1437
  return;
543
1438
  seen.add(f);
544
1439
  const t = this.typesByFqn.get(f);
545
- if (t && t.methods.has(method) && t.kind !== 'interface' && !this.isBodyless(t, method)) {
546
- out.push(f);
1440
+ if (t && t.kind !== 'interface' && self) {
1441
+ const own = (t.overloads.get(o.name) ?? []).find(x => !x.isConstructor && this.sameErasure({ owner: t, overload: x }, self));
1442
+ if (own && !this.isBodyless(t, own))
1443
+ out.push(f);
547
1444
  }
548
1445
  for (const sub of this.subtypes.get(f) ?? [])
549
1446
  walk(sub, seen);
@@ -702,13 +1599,11 @@ export class NavigationIndex {
702
1599
  }
703
1600
  // -------------------------------------------------------------- plumbing
704
1601
  /** A site whose method name could be the one asked for. */
705
- nameCouldMatch(call, wantedName, symbol, rec) {
1602
+ nameCouldMatch(call, wantedName, owner) {
706
1603
  if (wantedName === '<init>') {
707
- const typeFqn = stripLast(symbol);
708
- const t = this.typesByFqn.get(typeFqn);
1604
+ const t = this.typesByFqn.get(owner);
709
1605
  return !!t && (call.is_constructor === true || call.method_name === t.simpleName);
710
1606
  }
711
- void rec;
712
1607
  return call.method_name === wantedName;
713
1608
  }
714
1609
  querySymbol(query, kind) {
@@ -729,17 +1624,9 @@ export class NavigationIndex {
729
1624
  if (kind === 'callers') {
730
1625
  if (!call)
731
1626
  return undefined;
732
- return this.bind(call, rec)?.target;
733
- }
734
- const t = this.enclosingTypeAt(rec, query.site.line);
735
- if (!t)
736
- return undefined;
737
- for (const [name, m] of t.methods) {
738
- if (query.site.line >= m.startLine && query.site.line <= m.endLine) {
739
- return `${t.fqn}.${name}`;
740
- }
1627
+ return this.symbolOfCall(call, rec);
741
1628
  }
742
- return undefined;
1629
+ return this.methodAt(rec, query.site.line);
743
1630
  }
744
1631
  siteOf(call, rec) {
745
1632
  const site = {
@@ -753,11 +1640,16 @@ export class NavigationIndex {
753
1640
  site.text = text.trim();
754
1641
  return site;
755
1642
  }
756
- finish(kind, query, answers, unresolved, t0, opts) {
1643
+ finish(kind, query, answers, unresolved, t0, opts, extra = {}, scope = this.scope) {
757
1644
  let kept = answers;
758
1645
  if (opts.tiers)
759
1646
  kept = kept.filter(a => opts.tiers.includes(a.tier));
760
- kept = [...kept].sort((a, b) => a.site.file.localeCompare(b.site.file) || a.site.line - b.site.line);
1647
+ // Answers to an overloaded method arrive grouped per overload; the order
1648
+ // of the groups is the order of `overloads`, i.e. of declaration.
1649
+ const group = new Map((extra.overloads ?? []).map((o, i) => [o.signature, i]));
1650
+ kept = [...kept].sort((a, b) => (group.get(a.signature ?? '') ?? 0) - (group.get(b.signature ?? '') ?? 0) ||
1651
+ a.site.file.localeCompare(b.site.file) ||
1652
+ a.site.line - b.site.line);
761
1653
  const total = kept.length;
762
1654
  let truncated;
763
1655
  if (opts.limit !== undefined && total > opts.limit) {
@@ -768,7 +1660,7 @@ export class NavigationIndex {
768
1660
  query: { kind, ...(query.symbol ? { symbol: query.symbol } : { site: query.site }) },
769
1661
  answers: kept,
770
1662
  unresolved: [...unresolved].sort((a, b) => a.site.file.localeCompare(b.site.file) || a.site.line - b.site.line),
771
- scope: this.scope,
1663
+ scope,
772
1664
  timing: {
773
1665
  parseMs: round(this.parseMs),
774
1666
  indexMs: round(this.indexMs),
@@ -777,6 +1669,10 @@ export class NavigationIndex {
777
1669
  };
778
1670
  if (truncated)
779
1671
  out.truncated = truncated;
1672
+ if (extra.overloads)
1673
+ out.overloads = extra.overloads;
1674
+ if (extra.note)
1675
+ out.note = extra.note;
780
1676
  return out;
781
1677
  }
782
1678
  }
@@ -808,6 +1704,7 @@ function buildFileRecord(f) {
808
1704
  const tpkg = t.package ?? pkg;
809
1705
  const path = [tpkg, t.enclosing_type, t.name].filter(Boolean).join('.');
810
1706
  const methods = new Map();
1707
+ const overloads = new Map();
811
1708
  for (const m of t.methods ?? []) {
812
1709
  // A later overload does not replace an earlier one's range; the widest
813
1710
  // range wins, so a callee query over the method's body sees all of it.
@@ -818,6 +1715,20 @@ function buildFileRecord(f) {
818
1715
  endLine: prev ? Math.max(prev.endLine, m.end_line) : m.end_line,
819
1716
  modifiers: [...new Set([...(prev?.modifiers ?? []), ...(m.modifiers ?? [])])],
820
1717
  });
1718
+ const params = (m.parameters ?? []).map(p => p.type ?? '?');
1719
+ // The extraction reports a constructor's return type as `null` and a
1720
+ // method's as at least `void`; a constructor is named after its type.
1721
+ const isConstructor = m.return_type === null && m.name === t.name;
1722
+ push(overloads, m.name, {
1723
+ name: m.name,
1724
+ params,
1725
+ varargs: params.length > 0 && params[params.length - 1].endsWith('...'),
1726
+ returnType: m.return_type ?? null,
1727
+ isConstructor,
1728
+ startLine: m.start_line,
1729
+ endLine: m.end_line,
1730
+ modifiers: m.modifiers ?? [],
1731
+ });
821
1732
  }
822
1733
  types.push({
823
1734
  fqn: path,
@@ -826,11 +1737,15 @@ function buildFileRecord(f) {
826
1737
  isRecord: t.is_record === true,
827
1738
  file: f.path,
828
1739
  superNames: [t.extends, ...(t.implements ?? [])].filter((x) => !!x),
1740
+ extendsName: t.extends ?? null,
829
1741
  annotations: (t.annotations ?? []).map(stripAnnotation),
830
1742
  fieldNames: (t.fields ?? []).map(x => x.name),
1743
+ fieldTypes: new Map((t.fields ?? []).filter(x => x.type).map(x => [x.name, x.type])),
1744
+ typeParams: t.type_parameters ?? [],
831
1745
  getterFields: lombokFields(f.source, 'Getter'),
832
1746
  setterFields: lombokFields(f.source, 'Setter'),
833
1747
  methods,
1748
+ overloads,
834
1749
  startLine: t.start_line,
835
1750
  endLine: t.end_line,
836
1751
  });
@@ -858,6 +1773,140 @@ function lombokFields(source, kind) {
858
1773
  function stripAnnotation(a) {
859
1774
  return a.replace(/^@/, '').replace(/\(.*$/, '').split('.').pop() ?? a;
860
1775
  }
1776
+ /** `pkg.Type.method(A,B)` → its parts. The parameter list may hold generics with commas. */
1777
+ function parseSymbol(symbol) {
1778
+ const open = symbol.indexOf('(');
1779
+ const base = (open === -1 ? symbol : symbol.slice(0, open)).trim();
1780
+ const out = { base, owner: stripLast(base), name: lastSegment(base) };
1781
+ if (open !== -1) {
1782
+ const close = symbol.lastIndexOf(')');
1783
+ out.params = splitTopLevel(symbol.slice(open + 1, close === -1 ? undefined : close))
1784
+ .map(p => p.trim())
1785
+ .filter(Boolean);
1786
+ }
1787
+ return out;
1788
+ }
1789
+ /** Split on commas outside `<...>`. */
1790
+ function splitTopLevel(text) {
1791
+ const out = [];
1792
+ let depth = 0;
1793
+ let buf = '';
1794
+ for (const ch of text) {
1795
+ if (ch === '<')
1796
+ depth++;
1797
+ if (ch === '>')
1798
+ depth--;
1799
+ if (ch === ',' && depth === 0) {
1800
+ out.push(buf);
1801
+ buf = '';
1802
+ continue;
1803
+ }
1804
+ buf += ch;
1805
+ }
1806
+ out.push(buf);
1807
+ return out;
1808
+ }
1809
+ /** `(Long,String)` — simple names, generics erased, `String...` for varargs. */
1810
+ function signatureOf(o) {
1811
+ return `(${o.params.map(p => {
1812
+ const varargs = p.endsWith('...');
1813
+ let t = eraseType(p.replace(/@[\w.]+(\([^)]*\))?\s*/g, '').replace(/\bfinal\s+/g, '').replace(/\.\.\.$/, '').trim());
1814
+ let dims = '';
1815
+ while (t.endsWith('[]')) {
1816
+ dims += '[]';
1817
+ t = t.slice(0, -2).trim();
1818
+ }
1819
+ return `${t.split('.').pop() ?? t}${dims}${varargs ? '...' : ''}`;
1820
+ }).join(',')})`;
1821
+ }
1822
+ /** `pkg.Type.method`, or `pkg.Type.<init>` for a constructor. */
1823
+ function memberName(owner, o) {
1824
+ return o.isConstructor ? `${owner.fqn}.<init>` : `${owner.fqn}.${o.name}`;
1825
+ }
1826
+ /** Remove every `<...>` from a written type. */
1827
+ function eraseType(t) {
1828
+ let out = '';
1829
+ let depth = 0;
1830
+ for (const ch of t) {
1831
+ if (ch === '<') {
1832
+ depth++;
1833
+ continue;
1834
+ }
1835
+ if (ch === '>') {
1836
+ depth--;
1837
+ continue;
1838
+ }
1839
+ if (depth === 0)
1840
+ out += ch;
1841
+ }
1842
+ return out.trim();
1843
+ }
1844
+ /** Whether a call with `n` arguments can reach overload `o` at all. */
1845
+ function fitsArity(o, n) {
1846
+ if (o.params.length === n)
1847
+ return true;
1848
+ return o.varargs && n >= o.params.length - 1;
1849
+ }
1850
+ /**
1851
+ * The name of the outermost call an expression is, or nothing when it is not
1852
+ * a call: `a.b(x).c(y)` → `c`, `make(x)` → `make`, `a.<T>m()` → `m`.
1853
+ */
1854
+ function outerCallName(expr) {
1855
+ const text = expr.trim();
1856
+ if (!text.endsWith(')'))
1857
+ return undefined;
1858
+ let depth = 0;
1859
+ let open = -1;
1860
+ for (let i = text.length - 1; i >= 0; i--) {
1861
+ const ch = text[i];
1862
+ if (ch === ')')
1863
+ depth++;
1864
+ else if (ch === '(') {
1865
+ depth--;
1866
+ if (depth === 0) {
1867
+ open = i;
1868
+ break;
1869
+ }
1870
+ }
1871
+ }
1872
+ if (open <= 0)
1873
+ return undefined;
1874
+ const m = /([A-Za-z_$][\w$]*)\s*$/.exec(text.slice(0, open));
1875
+ if (!m)
1876
+ return undefined;
1877
+ const before = text.slice(0, open - m[0].length).trimEnd();
1878
+ // `new Foo(…)` is a constructor, typed by the extractor, not here.
1879
+ if (/\bnew$/.test(before))
1880
+ return undefined;
1881
+ return m[1];
1882
+ }
1883
+ /** Top-level arguments in an argument list's text, counted. */
1884
+ function countArgs(argText) {
1885
+ if (argText.trim() === '')
1886
+ return 0;
1887
+ let depth = 0;
1888
+ let quote = null;
1889
+ let n = 1;
1890
+ for (let i = 0; i < argText.length; i++) {
1891
+ const ch = argText[i];
1892
+ if (quote) {
1893
+ if (ch === quote && argText[i - 1] !== '\\')
1894
+ quote = null;
1895
+ continue;
1896
+ }
1897
+ if (ch === '"' || ch === "'") {
1898
+ quote = ch;
1899
+ continue;
1900
+ }
1901
+ if (ch === '(' || ch === '[' || ch === '{' || ch === '<')
1902
+ depth++;
1903
+ else if (ch === ')' || ch === ']' || ch === '}' || ch === '>')
1904
+ depth--;
1905
+ else if (ch === ',' && depth === 0)
1906
+ n++;
1907
+ }
1908
+ return n;
1909
+ }
861
1910
  /**
862
1911
  * Split a receiver expression into a root and a list of `.name(...)` steps.
863
1912
  *
@@ -899,14 +1948,19 @@ function splitCallChain(text) {
899
1948
  parts.push(buf);
900
1949
  if (parts.length === 0)
901
1950
  return null;
1951
+ const argcOf = (p) => {
1952
+ const open = p.indexOf('(');
1953
+ const close = p.lastIndexOf(')');
1954
+ return open === -1 || close < open ? 0 : countArgs(p.slice(open + 1, close));
1955
+ };
902
1956
  const head = parts[0].trim();
903
1957
  let root;
904
1958
  if (head === 'this')
905
- root = { kind: 'this', name: 'this' };
1959
+ root = { kind: 'this', name: 'this', argc: 0 };
906
1960
  else if (/^[A-Z]\w*$/.test(head))
907
- root = { kind: 'type', name: head };
1961
+ root = { kind: 'type', name: head, argc: 0 };
908
1962
  else if (/^[a-zA-Z_]\w*\s*\(/.test(head))
909
- root = { kind: 'call', name: head.slice(0, head.indexOf('(')).trim() };
1963
+ root = { kind: 'call', name: head.slice(0, head.indexOf('(')).trim(), argc: argcOf(head) };
910
1964
  else
911
1965
  return null; // a plain identifier, an index, an expression — not typed here
912
1966
  const steps = [];
@@ -915,7 +1969,7 @@ function splitCallChain(text) {
915
1969
  const m = /^([a-zA-Z_]\w*)\s*\(/.exec(p);
916
1970
  if (!m)
917
1971
  return null; // a field access mid-chain is not walked
918
- steps.push({ name: m[1], line: 0 });
1972
+ steps.push({ name: m[1], line: 0, argc: argcOf(p) });
919
1973
  }
920
1974
  return { root, steps };
921
1975
  }