-
Notifications
You must be signed in to change notification settings - Fork 1k
/
ExtractAPI.scala
903 lines (796 loc) · 36.9 KB
/
ExtractAPI.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
package dotty.tools.dotc
package sbt
import scala.language.unsafeNulls
import ExtractDependencies.internalError
import ast.{Positioned, Trees, tpd}
import core.*
import core.Decorators.*
import Annotations.*
import Contexts.*
import Flags.*
import Phases.*
import Trees.*
import Types.*
import Symbols.*
import Names.*
import StdNames.str
import NameOps.*
import inlines.Inlines
import transform.ValueClasses
import transform.Pickler
import dotty.tools.io.{File, FileExtension, JarArchive}
import util.{Property, SourceFile}
import java.io.PrintWriter
import ExtractAPI.NonLocalClassSymbolsInCurrentUnits
import scala.collection.mutable
import scala.util.hashing.MurmurHash3
import scala.util.chaining.*
/** This phase sends a representation of the API of classes to sbt via callbacks.
*
* This is used by sbt for incremental recompilation.
*
* See the documentation of `ExtractAPICollector`, `ExtractDependencies`,
* `ExtractDependenciesCollector` and
* http://www.scala-sbt.org/1.x/docs/Understanding-Recompilation.html for more
* information on incremental recompilation.
*
* The following flags affect this phase:
* -Yforce-sbt-phases
* -Ydump-sbt-inc
*
* @see ExtractDependencies
*/
class ExtractAPI extends Phase {
override def phaseName: String = ExtractAPI.name
override def description: String = ExtractAPI.description
override def isRunnable(using Context): Boolean = {
super.isRunnable && (ctx.runZincPhases || ctx.settings.YjavaTasty.value)
}
// Check no needed. Does not transform trees
override def isCheckable: Boolean = false
// when `-Yjava-tasty` is set we actually want to run this phase on Java sources
override def skipIfJava(using Context): Boolean = false
// SuperAccessors need to be part of the API (see the scripted test
// `trait-super` for an example where this matters), this is only the case
// after `PostTyper` (unlike `ExtractDependencies`, the simplication to trees
// done by `PostTyper` do not affect this phase because it only cares about
// definitions, and `PostTyper` does not change definitions).
override def runsAfter: Set[String] = Set(transform.Pickler.name)
override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] =
val doZincCallback = ctx.runZincPhases
val nonLocalClassSymbols = new mutable.HashSet[Symbol]
val units0 =
if doZincCallback then
val ctx0 = ctx.withProperty(NonLocalClassSymbolsInCurrentUnits, Some(nonLocalClassSymbols))
super.runOn(units)(using ctx0)
else
units // still run the phase for the side effects (writing TASTy files to -Yearly-tasty-output)
if doZincCallback then
ctx.withIncCallback(recordNonLocalClasses(nonLocalClassSymbols, _))
if ctx.settings.YjavaTasty.value then
units0.filterNot(_.typedAsJava) // remove java sources, this is the terminal phase when `-Yjava-tasty` is set
else
units0
end runOn
private def recordNonLocalClasses(nonLocalClassSymbols: mutable.HashSet[Symbol], cb: interfaces.IncrementalCallback)(using Context): Unit =
for cls <- nonLocalClassSymbols do
val sourceFile = cls.source
if sourceFile.exists && cls.isDefinedInCurrentRun then
recordNonLocalClass(cls, sourceFile, cb)
ctx.run.nn.asyncTasty.foreach(_.signalAPIComplete())
private def recordNonLocalClass(cls: Symbol, sourceFile: SourceFile, cb: interfaces.IncrementalCallback)(using Context): Unit =
def registerProductNames(fullClassName: String, binaryClassName: String) =
val pathToClassFile = s"${binaryClassName.replace('.', java.io.File.separatorChar)}.class"
val classFile = {
ctx.settings.outputDir.value match {
case jar: JarArchive =>
// important detail here, even on Windows, Zinc expects the separator within the jar
// to be the system default, (even if in the actual jar file the entry always uses '/').
// see https://github.com/sbt/zinc/blob/dcddc1f9cfe542d738582c43f4840e17c053ce81/internal/compiler-bridge/src/main/scala/xsbt/JarUtils.scala#L47
new java.io.File(s"$jar!$pathToClassFile")
case outputDir =>
new java.io.File(outputDir.file, pathToClassFile)
}
}
cb.generatedNonLocalClass(sourceFile, classFile.toPath(), binaryClassName, fullClassName)
end registerProductNames
val fullClassName = atPhase(sbtExtractDependenciesPhase) {
ExtractDependencies.classNameAsString(cls)
}
val binaryClassName = cls.binaryClassName
registerProductNames(fullClassName, binaryClassName)
// Register the names of top-level module symbols that emit two class files
val isTopLevelUniqueModule =
cls.owner.is(PackageClass) && cls.is(ModuleClass) && cls.companionClass == NoSymbol
if isTopLevelUniqueModule then
registerProductNames(fullClassName, binaryClassName.stripSuffix(str.MODULE_SUFFIX))
end recordNonLocalClass
override def run(using Context): Unit = {
val unit = ctx.compilationUnit
val sourceFile = unit.source
ctx.withIncCallback: cb =>
cb.startSource(sourceFile)
val nonLocalClassSymbols = ctx.property(NonLocalClassSymbolsInCurrentUnits).get
val apiTraverser = ExtractAPICollector(nonLocalClassSymbols)
val classes = apiTraverser.apiSource(unit.tpdTree)
val mainClasses = apiTraverser.mainClasses
if (ctx.settings.YdumpSbtInc.value) {
// Append to existing file that should have been created by ExtractDependencies
val pw = new PrintWriter(File(sourceFile.file.jpath).changeExtension(FileExtension.Inc).toFile
.bufferedWriter(append = true), true)
try {
classes.foreach(source => pw.println(DefaultShowAPI(source)))
} finally pw.close()
}
ctx.withIncCallback: cb =>
if !ctx.compilationUnit.suspendedAtInliningPhase then // already registered before this unit was suspended
classes.foreach(cb.api(sourceFile, _))
mainClasses.foreach(cb.mainClass(sourceFile, _))
}
}
object ExtractAPI:
val name: String = "sbt-api"
val description: String = "sends a representation of the API of classes to sbt"
private val NonLocalClassSymbolsInCurrentUnits: Property.Key[mutable.HashSet[Symbol]] = Property.Key()
/** Extracts full (including private members) API representation out of Symbols and Types.
*
* The exact representation used for each type is not important: the only thing
* that matters is that a binary-incompatible or source-incompatible change to
* the API (for example, changing the signature of a method, or adding a parent
* to a class) should result in a change to the API representation so that sbt
* can recompile files that depend on this API.
*
* Note that we only records types as they are defined and never "as seen from"
* some other prefix because `Types#asSeenFrom` is a complex operation and
* doing it for every inherited member would be slow, and because the number
* of prefixes can be enormous in some cases:
*
* class Outer {
* type T <: S
* type S
* class A extends Outer { /*...*/ }
* class B extends Outer { /*...*/ }
* class C extends Outer { /*...*/ }
* class D extends Outer { /*...*/ }
* class E extends Outer { /*...*/ }
* }
*
* `S` might be refined in an arbitrary way inside `A` for example, this
* affects the type of `T` as seen from `Outer#A`, so we could record that, but
* the class `A` also contains itself as a member, so `Outer#A#A#A#...` is a
* valid prefix for `T`. Even if we avoid loops, we still have a combinatorial
* explosion of possible prefixes, like `Outer#A#B#C#D#E`.
*
* It is much simpler to record `T` once where it is defined, but that means
* that the API representation of `T` may not change even though `T` as seen
* from some prefix has changed. This is why in `ExtractDependencies` we need
* to traverse used types to not miss dependencies, see the documentation of
* `ExtractDependencies#usedTypeTraverser`.
*
* TODO: sbt does not store the full representation that we compute, instead it
* hashes parts of it to reduce memory usage, then to see if something changed,
* it compares the hashes instead of comparing the representations. We should
* investigate whether we can just directly compute hashes in this phase
* without going through an intermediate representation, see
* http://www.scala-sbt.org/0.13/docs/Understanding-Recompilation.html#Hashing+an+API+representation
*/
private class ExtractAPICollector(nonLocalClassSymbols: mutable.HashSet[Symbol])(using Context) extends ThunkHolder {
import tpd.*
import xsbti.api
/** This cache is necessary for correctness, see the comment about inherited
* members in `apiClassStructure`
*/
private val classLikeCache = new mutable.HashMap[ClassSymbol, api.ClassLikeDef]
/** This cache is optional, it avoids recomputing representations */
private val typeCache = new mutable.HashMap[Type, api.Type]
/** This cache is necessary to avoid unstable name hashing when `typeCache` is present,
* see the comment in the `RefinedType` case in `computeType`
* The cache key is (api of RefinedType#parent, api of RefinedType#refinedInfo).
*/
private val refinedTypeCache = new mutable.HashMap[(api.Type, api.Definition), api.Structure]
/** This cache is necessary to avoid infinite loops when hashing an inline "Body" annotation.
* Its values are transitively seen inline references within a call chain starting from a single "origin" inline
* definition. Avoid hashing an inline "Body" annotation if its associated definition is already in the cache.
* Precondition: the cache is empty whenever we hash a new "origin" inline "Body" annotation.
*/
private val seenInlineCache = mutable.HashSet.empty[Symbol]
/** This cache is optional, it avoids recomputing hashes of inline "Body" annotations,
* e.g. when a concrete inline method is inherited by a subclass.
*/
private val inlineBodyCache = mutable.HashMap.empty[Symbol, Int]
private val allNonLocalClassesInSrc = new mutable.HashSet[xsbti.api.ClassLike]
private val _mainClasses = new mutable.HashSet[String]
private object Constants {
val emptyStringArray = Array[String]()
val local = api.ThisQualifier.create()
val public = api.Public.create()
val privateLocal = api.Private.create(local)
val protectedLocal = api.Protected.create(local)
val unqualified = api.Unqualified.create()
val thisPath = api.This.create()
val emptyType = api.EmptyType.create()
val emptyModifiers =
new api.Modifiers(false, false, false, false, false,false, false, false)
}
/** Some Dotty types do not have a corresponding type in xsbti.api.* that
* represents them. Until this is fixed we can workaround this by using
* special annotations that can never appear in the source code to
* represent these types.
*
* @param tp An approximation of the type we're trying to represent
* @param marker A special annotation to differentiate our type
*/
private def withMarker(tp: api.Type, marker: api.Annotation) =
api.Annotated.of(tp, Array(marker))
private def marker(name: String) =
api.Annotation.of(api.Constant.of(Constants.emptyType, name), Array())
private val orMarker = marker("Or")
private val byNameMarker = marker("ByName")
private val matchMarker = marker("Match")
private val superMarker = marker("Super")
/** Extract the API representation of a source file */
def apiSource(tree: Tree): Seq[api.ClassLike] = {
def apiClasses(tree: Tree): Unit = tree match {
case PackageDef(_, stats) =>
stats.foreach(apiClasses)
case tree: TypeDef =>
apiClass(tree.symbol.asClass)
case _ =>
}
apiClasses(tree)
forceThunks()
allNonLocalClassesInSrc.toSeq
}
def apiClass(sym: ClassSymbol): api.ClassLikeDef =
classLikeCache.getOrElseUpdate(sym, computeClass(sym))
def mainClasses: Set[String] = {
forceThunks()
_mainClasses.toSet
}
private def computeClass(sym: ClassSymbol): api.ClassLikeDef = {
import xsbti.api.{DefinitionType => dt}
val defType =
if (sym.is(Trait)) dt.Trait
else if (sym.is(ModuleClass)) {
if (sym.is(PackageClass)) dt.PackageModule
else dt.Module
} else dt.ClassDef
val selfType = apiType(sym.givenSelfType)
val name = sym.fullName.stripModuleClassSuffix.toString
// We strip module class suffix. Zinc relies on a class and its companion having the same name
val tparams = sym.typeParams.map(apiTypeParameter).toArray
val structure = apiClassStructure(sym)
val acc = apiAccess(sym)
val modifiers = apiModifiers(sym)
val anns = apiAnnotations(sym, inlineOrigin = NoSymbol).toArray
val topLevel = sym.isTopLevelClass
val childrenOfSealedClass = sym.sealedDescendants.sorted(classFirstSort).map(c =>
if (c.isClass)
apiType(c.typeRef)
else
apiType(c.termRef)
).toArray
val cl = api.ClassLike.of(
name, acc, modifiers, anns, defType, api.SafeLazy.strict(selfType), api.SafeLazy.strict(structure), Constants.emptyStringArray,
childrenOfSealedClass, topLevel, tparams)
allNonLocalClassesInSrc += cl
if !sym.isLocal then
nonLocalClassSymbols += sym
if (sym.isStatic && !sym.is(Trait) && ctx.platform.hasMainMethod(sym)) {
// If sym is an object, all main methods count, otherwise only @static ones count.
_mainClasses += name
}
api.ClassLikeDef.of(name, acc, modifiers, anns, tparams, defType)
}
def apiClassStructure(csym: ClassSymbol): api.Structure = {
val cinfo = csym.classInfo
val bases = {
val ancestorTypes0 =
try linearizedAncestorTypes(cinfo)
catch {
case ex: TypeError =>
// See neg/i1750a for an example where a cyclic error can arise.
// The root cause in this example is an illegal "override" of an inner trait
report.error(ex, csym.sourcePos)
defn.ObjectType :: Nil
}
if (csym.isDerivedValueClass) {
val underlying = ValueClasses.valueClassUnbox(csym).info.finalResultType
// The underlying type of a value class should be part of the name hash
// of the value class (see the test `value-class-underlying`), this is accomplished
// by adding the underlying type to the list of parent types.
underlying :: ancestorTypes0
} else
ancestorTypes0
}
val apiBases = bases.map(apiType)
// Synthetic methods that are always present do not affect the API
// and can therefore be ignored.
def alwaysPresent(s: Symbol) = csym.is(ModuleClass) && s.isConstructor
val decls = cinfo.decls.filter(!alwaysPresent(_))
val apiDecls = apiDefinitions(decls)
val declSet = decls.toSet
// TODO: We shouldn't have to compute inherited members. Instead, `Structure`
// should have a lazy `parentStructures` field.
val inherited = cinfo.baseClasses
.filter(bc => !bc.is(Scala2x))
.flatMap(_.classInfo.decls.filter(s => !(s.is(Private) || declSet.contains(s))))
// Inherited members need to be computed lazily because a class might contain
// itself as an inherited member, like in `class A { class B extends A }`,
// this works because of `classLikeCache`
val apiInherited = lzy(apiDefinitions(inherited).toArray)
api.Structure.of(api.SafeLazy.strict(apiBases.toArray), api.SafeLazy.strict(apiDecls.toArray), apiInherited)
}
def linearizedAncestorTypes(info: ClassInfo): List[Type] = {
val ref = info.appliedRef
// Note that the ordering of classes in `baseClasses` is important.
info.baseClasses.tail.map(ref.baseType)
}
// The hash generated by sbt for definitions is supposed to be symmetric so
// we shouldn't have to sort them, but it actually isn't symmetric for
// definitions which are classes, therefore we need to sort classes to
// ensure a stable hash.
// Modules and classes come first and are sorted by name, all other
// definitions come later and are not sorted.
private object classFirstSort extends Ordering[Symbol] {
override def compare(a: Symbol, b: Symbol) = {
val aIsClass = a.isClass
val bIsClass = b.isClass
if (aIsClass == bIsClass) {
if (aIsClass) {
if (a.is(Module) == b.is(Module))
a.fullName.toString.compareTo(b.fullName.toString)
else if (a.is(Module))
-1
else
1
} else
0
} else if (aIsClass)
-1
else
1
}
}
def apiDefinitions(defs: List[Symbol]): List[api.ClassDefinition] =
defs.sorted(classFirstSort).map(apiDefinition(_, inlineOrigin = NoSymbol))
/** `inlineOrigin` denotes an optional inline method that we are
* currently hashing the body of. If it exists, include extra information
* that is missing after erasure
*/
def apiDefinition(sym: Symbol, inlineOrigin: Symbol): api.ClassDefinition = {
if (sym.isClass) {
apiClass(sym.asClass)
} else if (sym.isType) {
apiTypeMember(sym.asType)
} else if (sym.is(Mutable, butNot = Accessor)) {
api.Var.of(sym.name.toString, apiAccess(sym), apiModifiers(sym),
apiAnnotations(sym, inlineOrigin).toArray, apiType(sym.info))
} else if (sym.isStableMember && !sym.isRealMethod) {
api.Val.of(sym.name.toString, apiAccess(sym), apiModifiers(sym),
apiAnnotations(sym, inlineOrigin).toArray, apiType(sym.info))
} else {
apiDef(sym.asTerm, inlineOrigin)
}
}
/** `inlineOrigin` denotes an optional inline method that we are
* currently hashing the body of. If it exists, include extra information
* that is missing after erasure
*/
def apiDef(sym: TermSymbol, inlineOrigin: Symbol): api.Def = {
var seenInlineExtras = false
var inlineExtras = 41
def mixInlineParam(p: Symbol): Unit =
if inlineOrigin.exists && p.is(Inline) then
seenInlineExtras = true
inlineExtras = hashInlineParam(p, inlineExtras)
def inlineExtrasAnnot: Option[api.Annotation] =
val h = inlineExtras
Option.when(seenInlineExtras) {
marker(s"${MurmurHash3.finalizeHash(h, "inlineExtras".hashCode)}")
}
def tparamList(pt: TypeLambda): List[api.TypeParameter] =
pt.paramNames.lazyZip(pt.paramInfos).map((pname, pbounds) =>
apiTypeParameter(pname.toString, 0, pbounds.lo, pbounds.hi)
)
def paramList(mt: MethodType, params: List[Symbol]): api.ParameterList =
val apiParams = params.lazyZip(mt.paramInfos).map((param, ptype) =>
mixInlineParam(param)
api.MethodParameter.of(
param.name.toString, apiType(ptype), param.is(HasDefault), api.ParameterModifier.Plain))
api.ParameterList.of(apiParams.toArray, mt.isImplicitMethod)
def paramLists(t: Type, paramss: List[List[Symbol]]): List[api.ParameterList] = t match {
case pt: TypeLambda =>
paramLists(pt.resultType, paramss.drop(1))
case mt @ MethodTpe(pnames, ptypes, restpe) =>
assert(paramss.nonEmpty && paramss.head.hasSameLengthAs(pnames),
i"mismatch for $sym, ${sym.info}, ${sym.paramSymss}")
paramList(mt, paramss.head) :: paramLists(restpe, paramss.tail)
case _ =>
Nil
}
/** returns list of pairs of 1: the position in all parameter lists, and 2: a type parameter list */
def tparamLists(t: Type, index: Int): List[(Int, List[api.TypeParameter])] = t match
case pt: TypeLambda =>
(index, tparamList(pt)) :: tparamLists(pt.resultType, index + 1)
case mt: MethodType =>
tparamLists(mt.resultType, index + 1)
case _ =>
Nil
val (tparams, tparamsExtras) = sym.info match
case pt: TypeLambda =>
(tparamList(pt), tparamLists(pt.resultType, index = 1))
case mt: MethodType =>
(Nil, tparamLists(mt.resultType, index = 1))
case _ =>
(Nil, Nil)
val vparamss = paramLists(sym.info, sym.paramSymss)
val retTp = sym.info.finalResultType.widenExpr
val tparamsExtraAnnot = Option.when(tparamsExtras.nonEmpty) {
marker(s"${hashTparamsExtras(tparamsExtras)("tparamsExtra".hashCode)}")
}
val annotations = inlineExtrasAnnot ++: tparamsExtraAnnot ++: apiAnnotations(sym, inlineOrigin)
api.Def.of(sym.zincMangledName.toString, apiAccess(sym), apiModifiers(sym),
annotations.toArray, tparams.toArray, vparamss.toArray, apiType(retTp))
}
def apiTypeMember(sym: TypeSymbol): api.TypeMember = {
val typeParams = Array[api.TypeParameter]()
val name = sym.name.toString
val access = apiAccess(sym)
val modifiers = apiModifiers(sym)
val as = apiAnnotations(sym, inlineOrigin = NoSymbol)
val tpe = sym.info
if (sym.isAliasType)
api.TypeAlias.of(name, access, modifiers, as.toArray, typeParams, apiType(tpe.bounds.hi))
else {
assert(sym.isAbstractOrParamType)
api.TypeDeclaration.of(name, access, modifiers, as.toArray, typeParams, apiType(tpe.bounds.lo), apiType(tpe.bounds.hi))
}
}
// Hack to represent dotty types which don't have an equivalent in xsbti
def combineApiTypes(apiTps: api.Type*): api.Type = {
api.Structure.of(api.SafeLazy.strict(apiTps.toArray),
api.SafeLazy.strict(Array()), api.SafeLazy.strict(Array()))
}
def apiType(tp: Type): api.Type = {
typeCache.getOrElseUpdate(tp, computeType(tp))
}
private def computeType(tp: Type): api.Type = {
// TODO: Never dealias. We currently have to dealias because
// sbt main class discovery relies on the signature of the main
// method being fully dealiased. See https://github.com/sbt/zinc/issues/102
val tp2 = if (!tp.isLambdaSub) tp.dealiasKeepAnnots else tp
tp2 match {
case NoPrefix | NoType =>
Constants.emptyType
case tp: NamedType =>
val sym = tp.symbol
// A type can sometimes be represented by multiple different NamedTypes
// (they will be `=:=` to each other, but not `==`), and the compiler
// may choose to use any of these representation, there is no stability
// guarantee. We avoid this instability by always normalizing the
// prefix: if it's a package, if we didn't do this sbt might conclude
// that some API changed when it didn't, leading to overcompilation
// (recompiling more things than what is needed for incremental
// compilation to be correct).
val prefix = if (sym.maybeOwner.is(Package)) // { type T } here T does not have an owner
sym.owner.thisType
else
tp.prefix
api.Projection.of(apiType(prefix), sym.name.toString)
case AppliedType(tycon, args) =>
def processArg(arg: Type): api.Type = arg match {
case arg @ TypeBounds(lo, hi) => // Handle wildcard parameters
if (lo.isDirectRef(defn.NothingClass) && hi.isDirectRef(defn.AnyClass))
Constants.emptyType
else {
val name = "_"
val ref = api.ParameterRef.of(name)
api.Existential.of(ref,
Array(apiTypeParameter(name, 0, lo, hi)))
}
case _ =>
apiType(arg)
}
val apiTycon = apiType(tycon)
val apiArgs = args.map(processArg)
api.Parameterized.of(apiTycon, apiArgs.toArray)
case tl: TypeLambda =>
val apiTparams = tl.typeParams.map(apiTypeParameter)
val apiRes = apiType(tl.resType)
api.Polymorphic.of(apiRes, apiTparams.toArray)
case rt: RefinedType =>
val name = rt.refinedName.toString
val parent = apiType(rt.parent)
def typeRefinement(name: String, tp: TypeBounds): api.TypeMember = tp match {
case TypeAlias(alias) =>
api.TypeAlias.of(name,
Constants.public, Constants.emptyModifiers, Array(), Array(), apiType(alias))
case TypeBounds(lo, hi) =>
api.TypeDeclaration.of(name,
Constants.public, Constants.emptyModifiers, Array(), Array(), apiType(lo), apiType(hi))
}
val decl = rt.refinedInfo match {
case rinfo: TypeBounds =>
typeRefinement(name, rinfo)
case _ =>
report.debuglog(i"sbt-api: skipped structural refinement in $rt")
null
}
// Aggressive caching for RefinedTypes: `typeCache` is enough as long as two
// RefinedType are `==`, but this is only the case when their `refinedInfo`
// are `==` and this is not always the case, consider:
//
// val foo: { type Bla = a.b.T }
// val bar: { type Bla = a.b.T }
//
// The sbt API representations of `foo` and `bar` (let's call them `apiFoo`
// and `apiBar`) will both be instances of `Structure`. If `typeCache` was
// the only cache, then in some cases we would have `apiFoo eq apiBar` and
// in other cases we would just have `apiFoo == apiBar` (this happens
// because the dotty representation of `a.b.T` is unstable, see the comment
// in the `NamedType` case above).
//
// The fact that we may or may not have `apiFoo eq apiBar` is more than
// an optimisation issue: it will determine whether the sbt name hash for
// `Bla` contains one or two entries (because sbt `NameHashing` will not
// traverse both `apiFoo` and `apiBar` if they are `eq`), therefore the
// name hash of `Bla` will be unstable, unless we make sure that
// `apiFoo == apiBar` always imply `apiFoo eq apiBar`. This is what
// `refinedTypeCache` is for.
refinedTypeCache.getOrElseUpdate((parent, decl), {
val adecl: Array[api.ClassDefinition] = if (decl == null) Array() else Array(decl)
api.Structure.of(api.SafeLazy.strict(Array(parent)), api.SafeLazy.strict(adecl), api.SafeLazy.strict(Array()))
})
case tp: RecType =>
apiType(tp.parent)
case RecThis(recType) =>
// `tp` must be present inside `recType`, so calling `apiType` on
// `recType` would lead to an infinite recursion, we avoid this by
// computing the representation of `recType` lazily.
apiLazy(recType)
case tp: AndType =>
combineApiTypes(apiType(tp.tp1), apiType(tp.tp2))
case tp: OrType =>
val s = combineApiTypes(apiType(tp.tp1), apiType(tp.tp2))
withMarker(s, orMarker)
case tp: FlexibleType =>
apiType(tp.underlying)
case ExprType(resultType) =>
withMarker(apiType(resultType), byNameMarker)
case MatchType(bound, scrut, cases) =>
val s = combineApiTypes(apiType(bound) :: apiType(scrut) :: cases.map(apiType)*)
withMarker(s, matchMarker)
case ConstantType(constant) =>
api.Constant.of(apiType(constant.tpe), constant.stringValue)
case AnnotatedType(tpe, annot) =>
api.Annotated.of(apiType(tpe), Array(apiAnnotation(annot)))
case tp: ThisType =>
apiThis(tp.cls)
case tp: ParamRef =>
// TODO: Distinguishing parameters based on their names alone is not enough,
// the binder is also needed (at least for type lambdas).
api.ParameterRef.of(tp.paramName.toString)
case tp: LazyRef =>
apiType(tp.ref)
case tp: TypeVar =>
apiType(tp.underlying)
case SuperType(thistpe, supertpe) =>
val s = combineApiTypes(apiType(thistpe), apiType(supertpe))
withMarker(s, superMarker)
case _ => {
internalError(i"Unhandled type $tp of class ${tp.getClass}")
Constants.emptyType
}
}
}
def apiLazy(tp: => Type): api.Type = {
// TODO: The sbt api needs a convenient way to make a lazy type.
// For now, we repurpose Structure for this.
val apiTp = lzy(Array(apiType(tp)))
api.Structure.of(apiTp, api.SafeLazy.strict(Array()), api.SafeLazy.strict(Array()))
}
def apiThis(sym: Symbol): api.Singleton = {
val pathComponents = sym.ownersIterator.takeWhile(!_.isEffectiveRoot)
.map(s => api.Id.of(s.name.toString))
api.Singleton.of(api.Path.of(pathComponents.toArray.reverse ++ Array(Constants.thisPath)))
}
def apiTypeParameter(tparam: ParamInfo): api.TypeParameter =
apiTypeParameter(tparam.paramName.toString, tparam.paramVarianceSign,
tparam.paramInfo.bounds.lo, tparam.paramInfo.bounds.hi)
def apiTypeParameter(name: String, variance: Int, lo: Type, hi: Type): api.TypeParameter =
api.TypeParameter.of(name, Array(), Array(), apiVariance(variance),
apiType(lo), apiType(hi))
def apiVariance(v: Int): api.Variance = {
import api.Variance.*
if (v < 0) Contravariant
else if (v > 0) Covariant
else Invariant
}
def apiAccess(sym: Symbol): api.Access = {
// Symbols which are private[foo] do not have the flag Private set,
// but their `privateWithin` exists, see `Parsers#ParserCommon#normalize`.
if (!sym.isOneOf(Protected | Private) && !sym.privateWithin.exists)
Constants.public
else if (sym.isAllOf(PrivateLocal))
Constants.privateLocal
else if (sym.isAllOf(ProtectedLocal))
Constants.protectedLocal
else {
val qualifier =
if (sym.privateWithin eq NoSymbol)
Constants.unqualified
else
api.IdQualifier.of(sym.privateWithin.fullName.toString)
if (sym.is(Protected))
api.Protected.of(qualifier)
else
api.Private.of(qualifier)
}
}
def apiModifiers(sym: Symbol): api.Modifiers = {
val absOver = sym.is(AbsOverride)
val abs = absOver || sym.isOneOf(Trait | Abstract | Deferred)
val over = absOver || sym.is(Override)
new api.Modifiers(abs, over, sym.is(Final), sym.is(Sealed),
sym.isOneOf(GivenOrImplicit), sym.is(Lazy), sym.is(Macro), sym.isSuperAccessor)
}
/** `inlineOrigin` denotes an optional inline method that we are
* currently hashing the body of.
*/
def apiAnnotations(s: Symbol, inlineOrigin: Symbol): List[api.Annotation] = {
val annots = new mutable.ListBuffer[api.Annotation]
val inlineBody = Inlines.bodyToInline(s)
if !inlineBody.isEmpty then
// If the body of an inline def changes, all the reverse dependencies of
// this method need to be recompiled. sbt has no way of tracking method
// bodies, so we include the hash of the body of the method as part of the
// signature we send to sbt.
def hash[U](inlineOrigin: Symbol): Int =
assert(seenInlineCache.add(s)) // will fail if already seen, guarded by treeHash
treeHash(inlineBody, inlineOrigin)
val inlineHash =
if inlineOrigin.exists then hash(inlineOrigin)
else inlineBodyCache.getOrElseUpdate(s, hash(inlineOrigin = s).tap(_ => seenInlineCache.clear()))
annots += marker(inlineHash.toString)
end if
// In the Scala2 ExtractAPI phase we only extract annotations that extend
// StaticAnnotation, but in Dotty we currently pickle all annotations so we
// extract everything, except:
// - annotations missing from the classpath which we simply skip over
// - inline body annotations which are handled above
// - the Child annotation since we already extract children via
// `api.ClassLike#childrenOfSealedClass` and adding this annotation would
// lead to overcompilation when using zinc's
// `IncOptions#useOptimizedSealed`.
s.annotations.foreach { annot =>
val sym = annot.symbol
if sym.exists && sym != defn.BodyAnnot && sym != defn.ChildAnnot then
annots += apiAnnotation(annot)
}
annots.toList
}
/** Produce a hash for a tree that is as stable as possible:
* it should stay the same across compiler runs, compiler instances,
* JVMs, etc.
*
* `inlineOrigin` denotes an optional inline method that we are hashing the body of, where `tree` could be
* its body, or the body of another method referenced in a call chain leading to `inlineOrigin`.
*
* If `inlineOrigin` is NoSymbol, then tree is the tree of an annotation.
*/
def treeHash(tree: Tree, inlineOrigin: Symbol): Int =
import core.Constants.*
def nameHash(n: Name, initHash: Int): Int =
val h =
if n.isTermName then
MurmurHash3.mix(initHash, TermNameHash)
else
MurmurHash3.mix(initHash, TypeNameHash)
// The hashCode of the name itself is not stable across compiler instances
MurmurHash3.mix(h, n.toString.hashCode)
end nameHash
def constantHash(c: Constant, initHash: Int): Int =
var h = MurmurHash3.mix(initHash, c.tag)
c.tag match
case NullTag =>
// No value to hash, the tag is enough.
case ClazzTag =>
// Go through `apiType` to get a value with a stable hash, it'd
// be better to use Murmur here too instead of relying on
// `hashCode`, but that would essentially mean duplicating
// https://github.com/sbt/zinc/blob/develop/internal/zinc-apiinfo/src/main/scala/xsbt/api/HashAPI.scala
// and at that point we might as well do type hashing on our own
// representation.
h = MurmurHash3.mix(h, apiType(c.typeValue).hashCode)
case _ =>
h = MurmurHash3.mix(h, c.value.hashCode)
h
end constantHash
def cannotHash(what: String, elem: Any, pos: Positioned): Unit =
internalError(i"Don't know how to produce a stable hash for $what", pos.sourcePos)
def positionedHash(p: ast.Positioned, initHash: Int): Int =
var h = initHash
p match
case p: WithLazyFields => p.forceFields()
case _ =>
if inlineOrigin.exists then
p match
case ref: RefTree @unchecked =>
val sym = ref.symbol
if sym.is(Inline, butNot = Param) && !seenInlineCache.contains(sym) then
// An inline method that calls another inline method will eventually inline the call
// at a non-inline callsite, in this case if the implementation of the nested call
// changes, then the callsite will have a different API, we should hash the definition
h = MurmurHash3.mix(h, apiDefinition(sym, inlineOrigin).hashCode)
case _ =>
// FIXME: If `p` is a tree we should probably take its type into account
// when hashing it, but producing a stable hash for a type is not trivial
// since the same type might have multiple representations, for method
// signatures this is already handled by `computeType` and the machinery
// in Zinc that generates hashes from that, if we can reliably produce
// stable hashes for types ourselves then we could bypass all that and
// send Zinc hashes directly.
h = MurmurHash3.mix(h, p.productPrefix.hashCode)
iteratorHash(p.productIterator, h)
end positionedHash
def iteratorHash(it: Iterator[Any], initHash: Int): Int =
var h = initHash
while it.hasNext do
it.next() match
case p: Positioned =>
h = positionedHash(p, h)
case xs: List[?] =>
h = iteratorHash(xs.iterator, h)
case c: Constant =>
h = constantHash(c, h)
case n: Name =>
h = nameHash(n, h)
case elem =>
cannotHash(what = i"`${elem.tryToShow}` of unknown class ${elem.getClass}", elem, tree)
h
end iteratorHash
val seed = 4 // https://xkcd.com/221
val h = positionedHash(tree, seed)
MurmurHash3.finalizeHash(h, 0)
end treeHash
/** Hash secondary type parameters in separate marker annotation.
* We hash them separately because the position of type parameters is important.
*/
private def hashTparamsExtras(tparamsExtras: List[(Int, List[api.TypeParameter])])(initHash: Int): Int =
def mixTparams(tparams: List[api.TypeParameter])(initHash: Int) =
var h = initHash
var elems = tparams
while elems.nonEmpty do
h = MurmurHash3.mix(h, elems.head.hashCode)
elems = elems.tail
h
def mixIndexAndTparams(index: Int, tparams: List[api.TypeParameter])(initHash: Int) =
mixTparams(tparams)(MurmurHash3.mix(initHash, index))
var h = initHash
var extras = tparamsExtras
var len = 0
while extras.nonEmpty do
h = mixIndexAndTparams(index = extras.head(0), tparams = extras.head(1))(h)
extras = extras.tail
len += 1
MurmurHash3.finalizeHash(h, len)
end hashTparamsExtras
/** Mix in the name hash also because otherwise switching which
* parameter is inline will not affect the hash.
*/
private def hashInlineParam(p: Symbol, h: Int) =
MurmurHash3.mix(p.name.toString.hashCode, MurmurHash3.mix(h, InlineParamHash))
def apiAnnotation(annot: Annotation): api.Annotation = {
// Like with inline defs, the whole body of the annotation and not just its
// type is part of its API so we need to store its hash, but Zinc wants us
// to extract the annotation type and its arguments, so we use a dummy
// annotation argument to store the hash of the tree. We still need to
// extract the annotation type in the way Zinc expects because sbt uses this
// information to find tests to run (for example junit tests are
// annotated @org.junit.Test).
api.Annotation.of(
apiType(annot.tree.tpe), // Used by sbt to find tests to run
Array(api.AnnotationArgument.of("TREE_HASH", treeHash(annot.tree, inlineOrigin = NoSymbol).toString)))
}
}