circle-ir 4.16.0 → 4.17.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 (59) hide show
  1. package/dist/analysis/constant-propagation/index.d.ts +2 -0
  2. package/dist/analysis/constant-propagation/index.d.ts.map +1 -1
  3. package/dist/analysis/constant-propagation/index.js +10 -0
  4. package/dist/analysis/constant-propagation/index.js.map +1 -1
  5. package/dist/analysis/constant-propagation/propagator.d.ts +11 -0
  6. package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
  7. package/dist/analysis/constant-propagation/propagator.js +125 -20
  8. package/dist/analysis/constant-propagation/propagator.js.map +1 -1
  9. package/dist/analysis/constant-propagation/types.d.ts +2 -0
  10. package/dist/analysis/constant-propagation/types.d.ts.map +1 -1
  11. package/dist/analysis/constant-propagation.d.ts +1 -1
  12. package/dist/analysis/constant-propagation.d.ts.map +1 -1
  13. package/dist/analysis/constant-propagation.js +1 -1
  14. package/dist/analysis/constant-propagation.js.map +1 -1
  15. package/dist/analysis/interprocedural.d.ts.map +1 -1
  16. package/dist/analysis/interprocedural.js +37 -8
  17. package/dist/analysis/interprocedural.js.map +1 -1
  18. package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
  19. package/dist/analysis/passes/cross-file-pass.js +3 -1
  20. package/dist/analysis/passes/cross-file-pass.js.map +1 -1
  21. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +13 -0
  22. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
  23. package/dist/analysis/passes/csharp-sanitizer-credit.js +48 -0
  24. package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
  25. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  26. package/dist/analysis/passes/language-sources-pass.js +80 -8
  27. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  28. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
  29. package/dist/analysis/passes/param-source-flow-gate-pass.js +17 -1
  30. package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
  31. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  32. package/dist/analysis/passes/sink-filter-pass.js +19 -3
  33. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  34. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  35. package/dist/analysis/passes/taint-propagation-pass.js +205 -22
  36. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  37. package/dist/analysis/rust-literal-guard.d.ts.map +1 -1
  38. package/dist/analysis/rust-literal-guard.js +100 -1
  39. package/dist/analysis/rust-literal-guard.js.map +1 -1
  40. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  41. package/dist/analysis/taint-matcher.js +1 -0
  42. package/dist/analysis/taint-matcher.js.map +1 -1
  43. package/dist/browser/circle-ir.js +561 -41
  44. package/dist/core/circle-ir-core.cjs +149 -17
  45. package/dist/core/circle-ir-core.js +149 -17
  46. package/dist/core/extractors/types.js +53 -9
  47. package/dist/core/extractors/types.js.map +1 -1
  48. package/dist/navigation/navigation-index.d.ts +40 -1
  49. package/dist/navigation/navigation-index.d.ts.map +1 -1
  50. package/dist/navigation/navigation-index.js +300 -45
  51. package/dist/navigation/navigation-index.js.map +1 -1
  52. package/dist/resolution/cross-file.d.ts +9 -0
  53. package/dist/resolution/cross-file.d.ts.map +1 -1
  54. package/dist/resolution/cross-file.js +134 -0
  55. package/dist/resolution/cross-file.js.map +1 -1
  56. package/dist/types/index.d.ts +18 -0
  57. package/dist/types/index.d.ts.map +1 -1
  58. package/dist/types/index.js.map +1 -1
  59. package/package.json +1 -1
@@ -56,6 +56,8 @@ const REFLECTIVE_METHODS = new Set([
56
56
  'getDeclaredConstructor', 'newProxyInstance', 'getAnnotation',
57
57
  'getAnnotationsByType', 'isAnnotationPresent', 'getGenericReturnType',
58
58
  ]);
59
+ /** What every class inherits from `java.lang.Object`. */
60
+ const OBJECT_METHODS = new Set(['toString', 'hashCode', 'equals', 'getClass', 'clone', 'finalize', 'notify', 'notifyAll', 'wait']);
59
61
  const LOMBOK_ACCESSOR = new Set(['Getter', 'Setter', 'Data', 'Value']);
60
62
  const LOMBOK_BUILDER = new Set(['Builder', 'SuperBuilder']);
61
63
  const PRIMITIVES = new Set(['byte', 'short', 'char', 'int', 'long', 'float', 'double', 'boolean']);
@@ -114,7 +116,7 @@ const JAVA_LANG = new Set([
114
116
  'Object', 'Number', 'Class', 'StringBuilder', 'CharSequence', 'Comparable', 'Iterable', 'Enum',
115
117
  ]);
116
118
  /** Bumped when the shape of a cached per-file record changes. */
117
- const CACHE_VERSION = 'nav-3';
119
+ const CACHE_VERSION = 'nav-4';
118
120
  const fileCache = new Map();
119
121
  export function buildNavigationIndex(files, options = {}) {
120
122
  return new NavigationIndex(files, options);
@@ -231,6 +233,15 @@ export class NavigationIndex {
231
233
  `declared: ${declared.map(o => `${q.base}${signatureOf(o)}`).join(', ')}`;
232
234
  return this.finish('callers', query, answers, unresolved, t0, opts, extra);
233
235
  }
236
+ if (target && declared.length === 0 && q.name !== '<init>' && this.visibleOverloads(target.fqn, q.name).length === 0) {
237
+ // `MapIterator.toString()`: the type is in the tree, the method is
238
+ // not — it is inherited from outside, so no call site can bind to the
239
+ // query's target, and "no callers" would read as a finding.
240
+ extra.note =
241
+ `${q.owner} declares no method ${q.name}, and no supertype in the searched tree does: it is inherited from ` +
242
+ `${OBJECT_METHODS.has(q.name) ? 'java.lang.Object' : 'a type outside the tree'}, so no call site here binds to ${q.base}. ` +
243
+ `A call reached only through string conversion or library code has no call site at all.`;
244
+ }
234
245
  const wantedSet = new Set(wanted);
235
246
  const dispatchOpen = target !== undefined && q.name !== '<init>' && wanted.some(o => this.dispatchIsOpen(target, o));
236
247
  for (const rec of this.files.values()) {
@@ -454,7 +465,7 @@ export class NavigationIndex {
454
465
  const encl = this.enclosingTypeOf(call, rec);
455
466
  const sup = encl?.extendsName ? this.resolveTypeName(encl.extendsName, rec) : undefined;
456
467
  if (sup && this.typesByFqn.has(sup)) {
457
- return this.bindInType(sup, call, rec, depth, `super call inside ${encl.fqn}`, sup);
468
+ return this.bindInType(sup, call, rec, depth, `super call inside ${encl.fqn}`, sup, true);
458
469
  }
459
470
  return undefined;
460
471
  }
@@ -500,7 +511,7 @@ export class NavigationIndex {
500
511
  return undefined;
501
512
  }
502
513
  /** Bind `call` to a member of `fqn` or of a supertype of it, choosing the overload. */
503
- bindInType(fqn, call, rec, depth, evidence, receiver) {
514
+ bindInType(fqn, call, rec, depth, evidence, receiver, nonVirtual = false) {
504
515
  const cands = this.visibleOverloads(fqn, call.method_name);
505
516
  if (cands.length === 0)
506
517
  return undefined;
@@ -520,8 +531,8 @@ export class NavigationIndex {
520
531
  overload,
521
532
  receiver,
522
533
  result: overloaded
523
- ? this.tierFor(owner, call.method_name, overload, `${evidence}; ${sel.how}`, true)
524
- : this.tierFor(owner, call.method_name, overload, evidence),
534
+ ? this.tierFor(owner, call.method_name, overload, `${evidence}; ${sel.how}`, true, nonVirtual)
535
+ : this.tierFor(owner, call.method_name, overload, evidence, false, nonVirtual),
525
536
  };
526
537
  }
527
538
  /**
@@ -535,11 +546,14 @@ export class NavigationIndex {
535
546
  * the first; what runs there is a subclass's body or a lambda, never the
536
547
  * member named. The target stays the declaring member, which is what a
537
548
  * static resolver answers; only the tier changes.
549
+ *
550
+ * A `super.m()` call is not dispatched: it runs the body it names, whatever
551
+ * overrides it below.
538
552
  */
539
- tierFor(owner, method, overload, evidence, overloaded = false) {
553
+ tierFor(owner, method, overload, evidence, overloaded = false, nonVirtual = false) {
540
554
  const impls = this.implementorsWith(owner.fqn, overload);
541
555
  const bodyless = this.isBodyless(owner, overload);
542
- if (impls.length > 1 || bodyless) {
556
+ if ((impls.length > 1 && !nonVirtual) || bodyless) {
543
557
  return {
544
558
  target: `${owner.fqn}.${method}`,
545
559
  tier: 'polymorphic',
@@ -652,11 +666,16 @@ export class NavigationIndex {
652
666
  if (byArity.length === 0)
653
667
  return { kind: 'none' };
654
668
  const args = call.arguments.map(a => this.argumentType(a, call, rec, depth));
669
+ // An implicit, `this.` or `super.` receiver is the enclosing type itself,
670
+ // which fixes what the declaring type's variables are.
671
+ const self = call.receiver === null || call.receiver === 'this' || call.receiver === 'super'
672
+ ? this.enclosingTypeOf(call, rec)
673
+ : undefined;
655
674
  if (byArity.length === 1) {
656
675
  // The only declaration in the tree at this arity, but the argument
657
676
  // types certainly rule it out: what is called is an overload this
658
677
  // index cannot see, inherited from a library type. Not ours.
659
- if ([1, 2, 3].every(p => this.applicable(byArity[0], args, p) === 'no'))
678
+ if ([1, 2, 3].every(p => this.applicable(byArity[0], args, p, self) === 'no'))
660
679
  return { kind: 'none' };
661
680
  return {
662
681
  kind: 'one',
@@ -666,7 +685,7 @@ export class NavigationIndex {
666
685
  }
667
686
  const fit = new Map();
668
687
  for (const c of byArity)
669
- fit.set(c, [1, 2, 3].map(p => this.applicable(c, args, p)));
688
+ fit.set(c, [1, 2, 3].map(p => this.applicable(c, args, p, self)));
670
689
  const possible = byArity.filter(c => fit.get(c).some(f => f !== 'no'));
671
690
  if (possible.length === 1) {
672
691
  return {
@@ -686,7 +705,7 @@ export class NavigationIndex {
686
705
  const C = P.filter(c => fit.get(c)[phase - 1] === 'yes');
687
706
  if (C.length === 0)
688
707
  return { kind: 'ambiguous', candidates: possible };
689
- const best = C.find(m => P.every(o => o === m || this.moreSpecific(m, o, phase)));
708
+ const best = C.find(m => P.every(o => o === m || this.moreSpecific(m, o, phase, self)));
690
709
  if (best) {
691
710
  return {
692
711
  kind: 'one',
@@ -699,10 +718,9 @@ export class NavigationIndex {
699
718
  return { kind: 'ambiguous', candidates: possible };
700
719
  }
701
720
  /** Whether `c` applies to arguments of these types in compiler phase 1, 2 or 3. */
702
- applicable(c, args, phase) {
721
+ applicable(c, args, phase, self) {
703
722
  const o = c.overload;
704
- const declRec = this.files.get(c.owner.file);
705
- const params = o.params.map(p => this.typeRef(p, declRec));
723
+ const params = this.paramRefsFrom(c, self);
706
724
  let worst = 'yes';
707
725
  const meet = (f) => {
708
726
  if (f === 'no')
@@ -718,17 +736,16 @@ export class NavigationIndex {
718
736
  }
719
737
  if (!o.varargs || args.length < params.length - 1)
720
738
  return 'no';
721
- const last = params[params.length - 1];
722
- const elem = { ...last, dims: Math.max(0, last.dims - 1) };
739
+ const elem = elementOf(params[params.length - 1]);
723
740
  args.forEach((a, i) => meet(this.fits(a, i < params.length - 1 ? params[i] : elem, 2)));
724
741
  return worst;
725
742
  }
726
743
  /** `m` is more specific than `o`: each of its parameter types fits the other's. */
727
- moreSpecific(m, o, phase) {
744
+ moreSpecific(m, o, phase, self) {
728
745
  if (phase === 3)
729
746
  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)));
747
+ const mp = this.paramRefsFrom(m, self);
748
+ const op = this.paramRefsFrom(o, self);
732
749
  if (mp.length !== op.length)
733
750
  return false;
734
751
  return mp.every((t, i) => this.fits(t, op[i], 1) === 'yes');
@@ -740,12 +757,42 @@ export class NavigationIndex {
740
757
  fits(a, p, phase) {
741
758
  if (!a)
742
759
  return 'maybe';
743
- if (p.typeVar)
760
+ if (p.opaque) {
761
+ if (a.simple === 'null' && a.dims === 0)
762
+ return 'yes';
763
+ if (a.typeVar)
764
+ return a.varName !== undefined && a.varName === p.varName && a.dims === p.dims ? 'yes' : 'maybe';
765
+ // Nothing but the variable itself is a subtype of it.
766
+ return a.prim || a.fqn ? 'no' : 'maybe';
767
+ }
768
+ if (p.typeVar) {
769
+ // Whatever fills a type variable is a subtype of its erasure, so an
770
+ // argument the erasure rules out is ruled out. One it admits is
771
+ // certainly admitted only where the compiler infers the variable from
772
+ // the arguments.
773
+ if (p.erasure) {
774
+ const f = this.fits(a, p.erasure, phase);
775
+ return f === 'no' ? 'no' : p.certain ? f : 'maybe';
776
+ }
744
777
  return a.prim && a.dims === 0 && phase === 1 ? 'no' : 'maybe';
778
+ }
745
779
  if (a.simple === 'null' && a.dims === 0)
746
780
  return p.prim && p.dims === 0 ? 'no' : 'yes';
747
- if (a.typeVar)
781
+ if (a.typeVar) {
782
+ // A value of a type variable is a value of its erasure, and maybe of
783
+ // something narrower.
784
+ // A type variable's supertypes are its bound's; what a generic call
785
+ // infers is only known to be below the erasure.
786
+ if (a.erasure) {
787
+ // An unbounded variable's only supertype is Object.
788
+ if (!a.inferred && a.erasure.fqn === 'java.lang.Object' && a.erasure.dims === 0) {
789
+ return !p.prim && p.dims === 0 && p.simple === 'Object' && (!p.fqn || p.fqn === 'java.lang.Object') ? 'yes' : 'no';
790
+ }
791
+ const f = this.fits(a.erasure, p, phase);
792
+ return a.inferred && f !== 'yes' ? 'maybe' : f;
793
+ }
748
794
  return 'maybe';
795
+ }
749
796
  if (a.dims > 0 || p.dims > 0) {
750
797
  if (a.dims === p.dims) {
751
798
  const ae = { ...a, dims: 0 };
@@ -895,7 +942,7 @@ export class NavigationIndex {
895
942
  */
896
943
  argumentType(arg, call, rec, depth) {
897
944
  if (arg.type)
898
- return this.typeRef(arg.type, rec);
945
+ return this.callSiteRef(arg.type, call, rec);
899
946
  if (!arg.location || depth >= 4)
900
947
  return null;
901
948
  const name = outerCallName(arg.expression);
@@ -908,6 +955,11 @@ export class NavigationIndex {
908
955
  let returned;
909
956
  let receiver;
910
957
  if (b?.kind === 'bound' && b.overload?.returnType) {
958
+ // `<M extends Map<K, V>> M invert(…)` returns whatever the call infers
959
+ // for `M`: known only to be some subtype of the erasure.
960
+ const own = this.paramRef(b.overload.returnType, { owner: b.owner, overload: b.overload });
961
+ if (own.varName && b.overload.typeParams.includes(own.varName))
962
+ return { ...own, inferred: true };
911
963
  returned = { text: b.overload.returnType, owner: b.owner };
912
964
  receiver = b.receiver;
913
965
  }
@@ -1068,37 +1120,165 @@ export class NavigationIndex {
1068
1120
  signatureMatches(o, params, owner) {
1069
1121
  if (o.params.length !== params.length)
1070
1122
  return false;
1071
- const rec = this.files.get(owner.file);
1072
- return o.params.every((p, i) => {
1123
+ const have = this.paramRefs({ owner, overload: o });
1124
+ return have.every((h, i) => {
1073
1125
  const want = this.typeRef(params[i], undefined);
1074
- const have = this.typeRef(p, rec);
1075
- if (want.dims !== have.dims)
1126
+ if (want.dims !== h.dims)
1076
1127
  return false;
1077
1128
  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;
1129
+ // A type variable is named as written (`K`) or by its erasure (`Object`,
1130
+ // `java.lang.Object`), the way the compiler and bytecode name it.
1131
+ if (h.varName && wantBare === h.varName)
1132
+ return true;
1133
+ return this.sameAsWritten(h, want, wantBare);
1084
1134
  });
1085
1135
  }
1086
- /** Same name and same erased parameter types: one overrides the other. */
1136
+ /** Whether a declared parameter type is the one a query wrote. */
1137
+ sameAsWritten(have, want, wantBare) {
1138
+ if (wantBare.includes('.') && have.fqn && !have.prim)
1139
+ return have.fqn === wantBare;
1140
+ return have.simple === want.simple;
1141
+ }
1142
+ /**
1143
+ * Same name and same parameter types: one overrides the other, as the
1144
+ * compiler decides it. Within one type that is the erasure, which the
1145
+ * compiler requires to differ between two declarations. Between a type and
1146
+ * its supertype, the supertype's parameter types are first read through the
1147
+ * subtype's supertype clauses — `Fn<T>.apply(T)` is `apply(String)` to a
1148
+ * class implementing `Fn<String>`, and `apply(T)` to one implementing
1149
+ * `Fn<T>`, which then does not override it with `apply(Collection<T>)` —
1150
+ * and the erasures compared. Between types neither of which is known to
1151
+ * extend the other, a type variable still matches whatever its erasure
1152
+ * admits.
1153
+ */
1087
1154
  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)
1155
+ if (a.overload.name !== b.overload.name || a.overload.params.length !== b.overload.params.length)
1091
1156
  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;
1157
+ if (a.owner !== b.owner) {
1158
+ const sub = this.isSubtype(a.owner.fqn, b.owner.fqn) ? a : this.isSubtype(b.owner.fqn, a.owner.fqn) ? b : undefined;
1159
+ if (sub)
1160
+ return this.overridesBySubstitution(sub, sub === a ? b : a);
1161
+ }
1162
+ const pa = this.paramRefs(a);
1163
+ const pb = this.paramRefs(b);
1164
+ const across = a.owner !== b.owner;
1165
+ const open = (t, c) => across && t.typeVar && (!t.varName || !c.overload.typeParams.includes(t.varName));
1166
+ return pa.every((x, i) => {
1167
+ const y = pb[i];
1168
+ if (x.dims !== y.dims)
1169
+ return false;
1170
+ const xo = open(x, a);
1171
+ const yo = open(y, b);
1172
+ if (xo && yo)
1173
+ return true;
1174
+ if (xo)
1175
+ return !x.erasure || this.fits({ ...y, dims: 0 }, { ...x.erasure, dims: 0 }, 1) !== 'no';
1176
+ if (yo)
1177
+ return !y.erasure || this.fits({ ...x, dims: 0 }, { ...y.erasure, dims: 0 }, 1) !== 'no';
1178
+ // A type variable no declaration in scope names: as lenient as before.
1179
+ if ((x.typeVar && !x.erasure) || (y.typeVar && !y.erasure))
1180
+ return true;
1181
+ return x.simple === y.simple;
1182
+ });
1183
+ }
1184
+ /** Whether `sub`'s declaration has `sup`'s parameter types as `sub`'s type sees them, erased. */
1185
+ overridesBySubstitution(sub, sup) {
1186
+ const mine = this.paramRefs(sub);
1187
+ const scope = this.scopeOf(sub.owner, sub.overload);
1188
+ return sup.overload.params.every((written, i) => {
1189
+ const own = sup.overload.vars[i];
1190
+ // The supertype method's own type variable: its erasure.
1191
+ const s = own && sup.overload.typeParams.includes(own.name) ? undefined : this.substitute(written, sup.owner, sub.owner.fqn);
1192
+ // No binding read for the variable (a raw supertype, or a clause this
1193
+ // index cannot resolve): the supertype's own erasure.
1194
+ const theirs = s
1195
+ ? this.withErasure(s.text, eraseWritten(s.text, scope), s.rec)
1196
+ : this.paramRef(written, sup);
1197
+ const m = mine[i];
1198
+ if (m.dims !== theirs.dims)
1199
+ return false;
1200
+ if (m.typeVar && !m.erasure)
1201
+ return true;
1202
+ if (theirs.typeVar && !theirs.erasure)
1203
+ return true;
1204
+ return m.simple === theirs.simple;
1100
1205
  });
1101
1206
  }
1207
+ /** A candidate's parameter types, each type variable carrying its erasure. */
1208
+ paramRefs(c) {
1209
+ return c.overload.params.map(p => this.paramRef(p, c));
1210
+ }
1211
+ /**
1212
+ * `c`'s parameter types as a call on `self` — the enclosing type, as an
1213
+ * implicit, `this.` or `super.` receiver — sees them: a variable of the
1214
+ * declaring type is that of `self` when `self` declares it, and what
1215
+ * `self`'s supertype clauses fill it with when it is inherited
1216
+ * (`Lru extends Base<String>` makes `Base<K>.put(K)` a `put(String)`).
1217
+ */
1218
+ paramRefsFrom(c, self) {
1219
+ const refs = this.paramRefs(c);
1220
+ if (!self)
1221
+ return refs;
1222
+ return refs.map((r, i) => {
1223
+ if (!r.typeVar || !r.varName || c.overload.typeParams.includes(r.varName))
1224
+ return r;
1225
+ if (c.owner === self)
1226
+ return { ...r, opaque: true };
1227
+ const sub = this.substitute(c.overload.params[i], c.owner, self.fqn);
1228
+ if (!sub)
1229
+ return r;
1230
+ const e = eraseWritten(sub.text, this.scopeOf(self, undefined));
1231
+ if (e)
1232
+ return { ...this.withErasure(sub.text, e, sub.rec), opaque: true };
1233
+ return this.typeRef(sub.text, sub.rec);
1234
+ });
1235
+ }
1236
+ /**
1237
+ * An argument's written type at a call site: a type variable of the
1238
+ * enclosing method or type carries its erasure.
1239
+ */
1240
+ callSiteRef(written, call, rec) {
1241
+ const encl = this.enclosingTypeOf(call, rec);
1242
+ if (!encl)
1243
+ return this.typeRef(written, rec);
1244
+ const line = call.location.line;
1245
+ let method;
1246
+ for (const list of encl.overloads.values()) {
1247
+ for (const o of list) {
1248
+ if (line < o.startLine || line > o.endLine)
1249
+ continue;
1250
+ if (!method || o.endLine - o.startLine < method.endLine - method.startLine)
1251
+ method = o;
1252
+ }
1253
+ }
1254
+ return this.withErasure(written, eraseWritten(written, this.scopeOf(encl, method)), rec);
1255
+ }
1256
+ /** One written parameter (or return) type of `c`, from its declaring file's point of view. */
1257
+ paramRef(written, c) {
1258
+ const i = c.overload.params.indexOf(written);
1259
+ const e = i !== -1 && c.overload.vars[i]
1260
+ ? { ...c.overload.vars[i], text: c.overload.erased[i] }
1261
+ : eraseWritten(written, this.scopeOf(c.owner, c.overload));
1262
+ return this.withErasure(written, e, this.files.get(c.owner.file));
1263
+ }
1264
+ /** A written type, with the erasure `eraseWritten` found for it when it is a type variable. */
1265
+ withErasure(written, e, rec) {
1266
+ if (!e)
1267
+ return this.typeRef(written, rec);
1268
+ const erased = this.typeRef(e.text, rec);
1269
+ return { ...erased, typeVar: true, varName: e.name, erasure: erased, certain: e.certain };
1270
+ }
1271
+ /** The type variables visible in a member of `owner` (`overload`), innermost first. */
1272
+ scopeOf(owner, overload) {
1273
+ const types = [];
1274
+ let t = owner;
1275
+ while (t) {
1276
+ types.push(t);
1277
+ const outer = this.typesByFqn.get(stripLast(t.fqn));
1278
+ t = outer && outer.file === owner.file ? outer : undefined;
1279
+ }
1280
+ return { method: overload ?? null, types };
1281
+ }
1102
1282
  // --------------------------------------------------------------- dispatch
1103
1283
  /**
1104
1284
  * How a site bound to `owner.overload` reaches `target`'s overload, if it
@@ -1728,6 +1908,10 @@ function buildFileRecord(f) {
1728
1908
  startLine: m.start_line,
1729
1909
  endLine: m.end_line,
1730
1910
  modifiers: m.modifiers ?? [],
1911
+ typeParams: m.type_parameters ?? [],
1912
+ typeBounds: m.type_parameter_bounds ?? {},
1913
+ erased: params,
1914
+ vars: params.map(() => null),
1731
1915
  });
1732
1916
  }
1733
1917
  types.push({
@@ -1742,6 +1926,7 @@ function buildFileRecord(f) {
1742
1926
  fieldNames: (t.fields ?? []).map(x => x.name),
1743
1927
  fieldTypes: new Map((t.fields ?? []).filter(x => x.type).map(x => [x.name, x.type])),
1744
1928
  typeParams: t.type_parameters ?? [],
1929
+ typeBounds: t.type_parameter_bounds ?? {},
1745
1930
  getterFields: lombokFields(f.source, 'Getter'),
1746
1931
  setterFields: lombokFields(f.source, 'Setter'),
1747
1932
  methods,
@@ -1750,6 +1935,22 @@ function buildFileRecord(f) {
1750
1935
  endLine: t.end_line,
1751
1936
  });
1752
1937
  }
1938
+ // Erase each parameter's type variables. Everything a declaration's
1939
+ // variables can name — the method, its type, the types enclosing it — is in
1940
+ // this file, so the result is per file and cached with it.
1941
+ for (const t of types) {
1942
+ const chain = [];
1943
+ for (let c = t; c; c = types.find(x => x.fqn === stripLast(c.fqn)))
1944
+ chain.push(c);
1945
+ for (const list of t.overloads.values()) {
1946
+ for (const o of list) {
1947
+ const scope = { method: o, types: chain };
1948
+ const es = o.params.map(p => eraseWritten(p, scope));
1949
+ o.erased = o.params.map((p, i) => es[i]?.text ?? p);
1950
+ o.vars = es.map(e => (e ? { name: e.name, certain: e.certain } : null));
1951
+ }
1952
+ }
1953
+ }
1753
1954
  return {
1754
1955
  path: f.path,
1755
1956
  language: f.language,
@@ -1806,9 +2007,13 @@ function splitTopLevel(text) {
1806
2007
  out.push(buf);
1807
2008
  return out;
1808
2009
  }
1809
- /** `(Long,String)` — simple names, generics erased, `String...` for varargs. */
2010
+ /**
2011
+ * `(Long,String)` — simple names, generics erased, `String...` for varargs, a
2012
+ * type variable shown as its erasure: `getNumber(Map,Object)` for
2013
+ * `<K> getNumber(Map<? super K, ?>, K)`, as javadoc and bytecode name it.
2014
+ */
1810
2015
  function signatureOf(o) {
1811
- return `(${o.params.map(p => {
2016
+ return `(${o.erased.map(p => {
1812
2017
  const varargs = p.endsWith('...');
1813
2018
  let t = eraseType(p.replace(/@[\w.]+(\([^)]*\))?\s*/g, '').replace(/\bfinal\s+/g, '').replace(/\.\.\.$/, '').trim());
1814
2019
  let dims = '';
@@ -1823,6 +2028,51 @@ function signatureOf(o) {
1823
2028
  function memberName(owner, o) {
1824
2029
  return o.isConstructor ? `${owner.fqn}.<init>` : `${owner.fqn}.${o.name}`;
1825
2030
  }
2031
+ /**
2032
+ * What a written type that is a type variable erases to, the way the compiler
2033
+ * erases it: its leftmost bound, erased in turn when the bound is itself a
2034
+ * variable (`W extends U`), else `Object`; array and varargs suffixes kept.
2035
+ * Nothing when it is not a type variable in scope. The innermost declaration
2036
+ * of a name hides the outer ones.
2037
+ *
2038
+ * `certain` says whether an argument that fits the erasure certainly fits the
2039
+ * variable: so for a method's own variable, which the compiler infers from
2040
+ * the arguments, unless its bound names the variable itself
2041
+ * (`T extends Comparable<T>`); never for a type's, which the receiver fixes.
2042
+ */
2043
+ function eraseWritten(written, scope, seen = new Set()) {
2044
+ let t = written.replace(/@[\w.]+(\([^)]*\))?\s*/g, '').replace(/\bfinal\s+/g, '').trim();
2045
+ let suffix = '';
2046
+ if (t.endsWith('...')) {
2047
+ suffix = '...';
2048
+ t = t.slice(0, -3).trim();
2049
+ }
2050
+ t = eraseType(t);
2051
+ while (t.endsWith('[]')) {
2052
+ suffix = `[]${suffix}`;
2053
+ t = t.slice(0, -2).trim();
2054
+ }
2055
+ if (!/^[A-Za-z_$][\w$]*$/.test(t) || seen.has(t))
2056
+ return undefined;
2057
+ let v;
2058
+ if (scope.method?.typeParams.includes(t))
2059
+ v = { bound: scope.method.typeBounds[t] ?? null, kind: 'method' };
2060
+ else {
2061
+ const owner = scope.types.find(x => x.typeParams.includes(t));
2062
+ if (owner)
2063
+ v = { bound: owner.typeBounds[t] ?? null, kind: 'type' };
2064
+ }
2065
+ if (!v)
2066
+ return undefined;
2067
+ if (v.bound === null)
2068
+ return { name: t, text: `Object${suffix}`, certain: v.kind === 'method' };
2069
+ const inner = eraseWritten(v.bound, scope, new Set([...seen, t]));
2070
+ if (inner)
2071
+ return { name: t, text: `${inner.text}${suffix}`, certain: v.kind === 'method' && inner.certain };
2072
+ const args = v.bound.includes('<') ? v.bound.slice(v.bound.indexOf('<') + 1) : '';
2073
+ const selfBounded = new RegExp(`\\b${t}\\b`).test(args);
2074
+ return { name: t, text: `${eraseType(v.bound)}${suffix}`, certain: v.kind === 'method' && !selfBounded };
2075
+ }
1826
2076
  /** Remove every `<...>` from a written type. */
1827
2077
  function eraseType(t) {
1828
2078
  let out = '';
@@ -1841,6 +2091,11 @@ function eraseType(t) {
1841
2091
  }
1842
2092
  return out.trim();
1843
2093
  }
2094
+ /** A varargs parameter's element type. */
2095
+ function elementOf(t) {
2096
+ const dims = Math.max(0, t.dims - 1);
2097
+ return { ...t, dims, ...(t.erasure ? { erasure: { ...t.erasure, dims } } : {}) };
2098
+ }
1844
2099
  /** Whether a call with `n` arguments can reach overload `o` at all. */
1845
2100
  function fitsArity(o, n) {
1846
2101
  if (o.params.length === n)