@@ -114,6 +114,11 @@ private class ExtractAPICollector(implicit val ctx: Context) extends ThunkHolder
114
114
private [this ] val classLikeCache = new mutable.HashMap [ClassSymbol , api.ClassLike ]
115
115
/** This cache is optional, it avoids recomputing representations */
116
116
private [this ] val typeCache = new mutable.HashMap [Type , api.Type ]
117
+ /** This cache is necessary to avoid unstable name hashing when `typeCache` is present,
118
+ * see the comment in the `RefinedType` case in `computeType`
119
+ * The cache key is (api of RefinedType#parent, api of RefinedType#refinedInfo).
120
+ */
121
+ private [this ] val refinedTypeCache = new mutable.HashMap [(api.Type , api.Definition ), api.Structure ]
117
122
118
123
private [this ] object Constants {
119
124
val emptyStringArray = Array [String ]()
@@ -349,7 +354,14 @@ private class ExtractAPICollector(implicit val ctx: Context) extends ThunkHolder
349
354
Constants .emptyType
350
355
case tp : NamedType =>
351
356
val sym = tp.symbol
352
- // Normalize package prefix to avoid instability of representation
357
+ // A type can sometimes be represented by multiple different NamedTypes
358
+ // (they will be `=:=` to each other, but not `==`), and the compiler
359
+ // may choose to use any of these representation, there is no stability
360
+ // guarantee. We avoid this instability by always normalizing the
361
+ // prefix: if it's a package, if we didn't do this sbt might conclude
362
+ // that some API changed when it didn't, leading to overcompilation
363
+ // (recompiling more things than what is needed for incremental
364
+ // compilation to be correct).
353
365
val prefix = if (sym.owner.is(Package ))
354
366
sym.owner.thisType
355
367
else
@@ -389,15 +401,39 @@ private class ExtractAPICollector(implicit val ctx: Context) extends ThunkHolder
389
401
new api.TypeDeclaration (apiType(lo), apiType(hi),
390
402
Array (), name, Constants .public, Constants .emptyModifiers, Array ())
391
403
}
392
-
393
- val decl : Array [api.Definition ] = rt.refinedInfo match {
404
+ val decl = rt.refinedInfo match {
394
405
case rinfo : TypeBounds =>
395
- Array ( typeRefinement(name, rinfo) )
406
+ typeRefinement(name, rinfo)
396
407
case _ =>
397
408
ctx.debuglog(i " sbt-api: skipped structural refinement in $rt" )
398
- Array ()
409
+ null
399
410
}
400
- new api.Structure (strict2lzy(Array (parent)), strict2lzy(decl), strict2lzy(Array ()))
411
+
412
+ // Aggressive caching for RefinedTypes: `typeCache` is enough as long as two
413
+ // RefinedType are `==`, but this is only the case when their `refinedInfo`
414
+ // are `==` and this is not always the case, consider:
415
+ //
416
+ // val foo: { type Bla = a.b.T }
417
+ // val bar: { type Bla = a.b.T }
418
+ //
419
+ // The sbt API representations of `foo` and `bar` (let's call them `apiFoo`
420
+ // and `apiBar`) will both be instances of `Structure`. If `typeCache` was
421
+ // the only cache, then in some cases we would have `apiFoo eq apiBar` and
422
+ // in other cases we would just have `apiFoo == apiBar` (this happens
423
+ // because the dotty representation of `a.b.T` is unstable, see the comment
424
+ // in the `NamedType` case above).
425
+ //
426
+ // The fact that we may or may not have `apiFoo eq apiBar` is more than
427
+ // an optimisation issue: it will determine whether the sbt name hash for
428
+ // `Bla` contains one or two entries (because sbt `NameHashing` will not
429
+ // traverse both `apiFoo` and `apiBar` if they are `eq`), therefore the
430
+ // name hash of `Bla` will be unstable, unless we make sure that
431
+ // `apiFoo == apiBar` always imply `apiFoo eq apiBar`. This is what
432
+ // `refinedTypeCache` is for.
433
+ refinedTypeCache.getOrElseUpdate((parent, decl), {
434
+ val adecl : Array [api.Definition ] = if (decl == null ) Array () else Array (decl)
435
+ new api.Structure (strict2lzy(Array (parent)), strict2lzy(adecl), strict2lzy(Array ()))
436
+ })
401
437
case tp : RecType =>
402
438
apiType(tp.parent)
403
439
case RecThis (recType) =>
0 commit comments