Skip to content

Commit 7122d06

Browse files
committed
WIP classpath plugin
1 parent 727964b commit 7122d06

File tree

6 files changed

+186
-15
lines changed

6 files changed

+186
-15
lines changed

src/compiler/scala/tools/nsc/backend/JavaPlatform.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ trait JavaPlatform extends Platform {
2020
private[nsc] var currentClassPath: Option[ClassPath] = None
2121

2222
private[nsc] def classPath: ClassPath = {
23-
if (currentClassPath.isEmpty) currentClassPath = Some(new PathResolver(settings).result)
23+
if (currentClassPath.isEmpty) currentClassPath = Some(applyClassPathPlugins(new PathResolver(settings).result))
2424
currentClassPath.get
2525
}
2626

src/compiler/scala/tools/nsc/backend/Platform.scala

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
package scala.tools.nsc
77
package backend
88

9+
import java.nio.ByteBuffer
10+
911
import io.AbstractFile
12+
import scala.tools.nsc.classpath.AggregateClassPath
1013
import scala.tools.nsc.util.ClassPath
1114

1215
/** The platform dependent pieces of Global.
@@ -37,5 +40,52 @@ trait Platform {
3740
* a re-compile is triggered. On .NET by contrast classfiles always take precedence.
3841
*/
3942
def needCompile(bin: AbstractFile, src: AbstractFile): Boolean
40-
}
4143

44+
/**
45+
* A class path plugin can modify the classpath before it is used by the compiler, and can
46+
* customize the way that the compiler reads the contents of class files.
47+
*
48+
* Applications could include:
49+
*
50+
* - Caching the ScalaSignature annotation contents, to avoid the cost of decompressing
51+
* and parsing the classfile, akin to the OpenJDK's .sig format for stripped class files.
52+
* - Starting a downstream compilation job immediately after the upstream job has completed
53+
* the pickler phase ("Build Pipelineing")
54+
*/
55+
abstract class ClassPathPlugin {
56+
def info(file: AbstractFile, clazz: ClassSymbol): Option[ClassfileInfo]
57+
def modifyClassPath(classPath: Seq[ClassPath]): Seq[ClassPath] = classPath
58+
}
59+
sealed abstract class ClassfileInfo {}
60+
final case class ClassBytes(data: ByteBuffer) extends ClassfileInfo
61+
final case class ScalaRawClass(className: String) extends ClassfileInfo
62+
final case class ScalaClass(className: String, pickle: ByteBuffer) extends ClassfileInfo
63+
64+
65+
/** A list of registered classpath plugins */
66+
private var classPathPlugins: List[ClassPathPlugin] = Nil
67+
68+
protected final def applyClassPathPlugins(original: ClassPath): ClassPath = {
69+
val entries = original match {
70+
case AggregateClassPath(entries) => entries
71+
case single => single :: Nil
72+
}
73+
val entries1 = classPathPlugins.foldLeft(entries) {
74+
(entries, plugin) => plugin.modifyClassPath(entries)
75+
}
76+
AggregateClassPath(entries1)
77+
}
78+
79+
80+
/** Registers a new classpath plugin */
81+
final def addClassPathPlugin(plugin: ClassPathPlugin): Unit = {
82+
if (!classPathPlugins.contains(plugin))
83+
classPathPlugins = plugin :: classPathPlugins
84+
}
85+
final def classFileInfo(file: AbstractFile, clazz: ClassSymbol): Option[ClassfileInfo] = if (classPathPlugins eq Nil) None else {
86+
classPathPlugins.foldLeft(Option.empty[ClassfileInfo]) {
87+
case (Some(info), _) => Some(info)
88+
case (None, plugin) => plugin.info(file, clazz)
89+
}
90+
}
91+
}

src/compiler/scala/tools/nsc/symtab/classfile/AbstractFileReader.scala

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,8 @@ import scala.tools.nsc.io.AbstractFile
1919
* @author Philippe Altherr
2020
* @version 1.0, 23/03/2004
2121
*/
22-
class AbstractFileReader(val file: AbstractFile) {
23-
24-
/** the buffer containing the file
25-
*/
26-
val buf: Array[Byte] = file.toByteArray
22+
class AbstractFileReader(val file: AbstractFile, val buf: Array[Byte]) {
23+
def this(file: AbstractFile) = this(file, file.toByteArray)
2724

2825
/** the current input pointer
2926
*/

src/compiler/scala/tools/nsc/symtab/classfile/ClassfileParser.scala

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ package tools.nsc
88
package symtab
99
package classfile
1010

11-
import java.io.{ByteArrayInputStream, DataInputStream, File, IOException}
11+
import java.io._
1212
import java.lang.Integer.toHexString
1313

1414
import scala.collection.{immutable, mutable}
1515
import scala.collection.mutable.{ArrayBuffer, ListBuffer}
1616
import scala.annotation.switch
1717
import scala.reflect.internal.JavaAccFlags
1818
import scala.reflect.internal.pickling.{ByteCodecs, PickleBuffer}
19-
import scala.reflect.io.NoAbstractFile
19+
import scala.reflect.io.{NoAbstractFile, VirtualFile}
2020
import scala.reflect.internal.util.Collections._
2121
import scala.tools.nsc.util.ClassPath
2222
import scala.tools.nsc.io.AbstractFile
@@ -145,14 +145,50 @@ abstract class ClassfileParser {
145145
def parse(file: AbstractFile, clazz: ClassSymbol, module: ModuleSymbol): Unit = {
146146
this.file = file
147147
pushBusy(clazz) {
148-
this.in = new AbstractFileReader(file)
149148
this.clazz = clazz
150149
this.staticModule = module
151150
this.isScala = false
152151

153-
parseHeader()
154-
this.pool = newConstantPool
155-
parseClass()
152+
import loaders.platform._
153+
classFileInfo(file, clazz) match {
154+
case Some(info) =>
155+
info match {
156+
case ScalaRawClass(className) =>
157+
isScalaRaw = true
158+
currentClass = TermName(className)
159+
case ScalaClass(className, pickle) =>
160+
isScala = true
161+
currentClass = TermName(className)
162+
if (pickle.hasArray) {
163+
unpickler.unpickle(pickle.array, pickle.arrayOffset + pickle.position(), clazz, staticModule, file.name)
164+
} else {
165+
val array = new Array[Byte](pickle.remaining)
166+
pickle.get(array)
167+
unpickler.unpickle(array, 0, clazz, staticModule, file.name)
168+
}
169+
case ClassBytes(data) =>
170+
val array = new Array[Byte](data.remaining)
171+
data.get(array)
172+
this.in = new AbstractFileReader(file, array)
173+
parseClass()
174+
}
175+
case None =>
176+
this.in = new AbstractFileReader(file)
177+
parseHeader()
178+
this.pool = new ConstantPool
179+
parseClass()
180+
}
181+
if (isScalaRaw && !isNothingOrNull) {
182+
unlinkRaw()
183+
}
184+
}
185+
}
186+
187+
private def unlinkRaw(): Unit = {
188+
val decls = clazz.enclosingPackage.info.decls
189+
for (c <- List(clazz, staticModule, staticModule.moduleClass)) {
190+
c.setInfo(NoType)
191+
decls.unlink(c)
156192
}
157193
}
158194

@@ -434,7 +470,18 @@ abstract class ClassfileParser {
434470
lookupClass(name)
435471
}
436472

437-
def parseClass() {
473+
// TODO: remove after the next 2.13 milestone
474+
// A bug in the backend caused classes ending in `$` do get only a Scala marker attribute
475+
// instead of a ScalaSig and a Signature annotaiton. This went unnoticed because isScalaRaw
476+
// classes were parsed like Java classes. The below covers the cases in the std lib.
477+
private def isNothingOrNull = {
478+
val n = clazz.fullName.toString
479+
n == "scala.runtime.Nothing$" || n == "scala.runtime.Null$"
480+
}
481+
482+
def parseClass(): Unit = {
483+
unpickleOrParseInnerClasses()
484+
438485
val jflags = readClassFlags()
439486
val sflags = jflags.toScalaFlags
440487
val nameIdx = u2
@@ -466,6 +513,24 @@ abstract class ClassfileParser {
466513
if (!c.isInstanceOf[StubSymbol] && c != clazz) mismatchError(c)
467514
}
468515

516+
if (isScala) {
517+
() // We're done
518+
} else if (isScalaRaw && !isNothingOrNull) {
519+
val decls = clazz.enclosingPackage.info.decls
520+
for (c <- List(clazz, staticModule, staticModule.moduleClass)) {
521+
c.setInfo(NoType)
522+
decls.unlink(c)
523+
}
524+
} else {
525+
val sflags = jflags.toScalaFlags // includes JAVA
526+
527+
def parseParents(): List[Type] = raiseLoaderLevel {
528+
val superType = pool.getSuperClass(u2).tpe_*
529+
val ifaceCount = u2
530+
val ifaces = for (i <- List.range(0, ifaceCount)) yield pool.getSuperClass(u2).tpe_*
531+
superType :: ifaces
532+
}
533+
469534
addEnclosingTParams(clazz)
470535
parseInnerClasses() // also sets the isScala / isScalaRaw flags, see r15956
471536
// get the class file parser to reuse scopes.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright (c) 2018 Lightbend. All rights reserved.
3+
*/
4+
package scala.tools.nsc.classpath
5+
6+
import java.nio.ByteBuffer
7+
8+
import org.junit.Assert.assertEquals
9+
import org.junit.Test
10+
import org.junit.runner.RunWith
11+
import org.junit.runners.JUnit4
12+
13+
import scala.reflect.io.VirtualDirectory
14+
import scala.tools.nsc.io.AbstractFile
15+
import scala.tools.nsc.symtab.SymbolTableForUnitTesting
16+
import scala.tools.nsc.util.ClassPath
17+
import scala.tools.testing.BytecodeTesting
18+
import scala.tools.testing.BytecodeTesting.makeSourceFile
19+
20+
@RunWith(classOf[JUnit4])
21+
class ClassPluginTest extends BytecodeTesting {
22+
// We use this.compiler to generate Scala pickles...
23+
override def compilerArgs = "-Ystop-after:pickler"
24+
25+
// ... and this one to read them with a ClassPathPlugin
26+
object symbolTable extends SymbolTableForUnitTesting {
27+
val fakeClasses = Map(
28+
"fake.C" -> platform.ScalaClass("fake.C", pickleOf("package fake; class C { def foo = 42 }"))
29+
)
30+
private val fakes = new VirtualDirectory("fakes", None)
31+
fakes.subdirectoryNamed("fake").fileNamed("C.class")
32+
33+
lazy val classpathPlugin = new platform.ClassPathPlugin {
34+
override def modifyClassPath(classPath: Seq[ClassPath]): Seq[ClassPath] = {
35+
// Add a classpath entry with the fake/C.class
36+
VirtualDirectoryClassPath(fakes) +: classPath
37+
}
38+
39+
override def info(file: AbstractFile, clazz: ClassSymbol): Option[platform.ClassfileInfo] =
40+
fakeClasses.get(clazz.fullNameString)
41+
}
42+
this.platform.addClassPathPlugin(classpathPlugin)
43+
}
44+
45+
@Test def classPathPluginTest(): Unit = {
46+
import symbolTable._
47+
val CClass = rootMirror.getRequiredClass("fake.C")
48+
val C_tpe = CClass.info
49+
assertEquals("def foo: Int", definitions.fullyInitializeSymbol(C_tpe.decl(TermName("foo"))).defString)
50+
}
51+
52+
private def pickleOf(code: String): ByteBuffer = {
53+
import compiler._
54+
val run = newRun
55+
run.compileSources(makeSourceFile(code, "unitTestSource.scala") :: Nil)
56+
val pickle = run.symData.toList.head._2
57+
ByteBuffer.wrap(pickle.bytes, 0, pickle.writeIndex)
58+
}
59+
}

test/junit/scala/tools/nsc/symtab/SymbolTableForUnitTesting.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class SymbolTableForUnitTesting extends SymbolTable {
3636

3737
def platformPhases: List[SubComponent] = Nil
3838

39-
private[nsc] lazy val classPath: ClassPath = new PathResolver(settings).result
39+
private[nsc] lazy val classPath: ClassPath = applyClassPathPlugins(new PathResolver(settings).result)
4040

4141
def isMaybeBoxed(sym: Symbol): Boolean = ???
4242
def needCompile(bin: AbstractFile, src: AbstractFile): Boolean = ???

0 commit comments

Comments
 (0)