Skip to content

Commit db5ca52

Browse files
authored
Merge pull request #3438 from dotty-staging/plugin
Fix #1519: Port compiler Plugin from scalac
2 parents 477ebee + f1a6923 commit db5ca52

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+1108
-87
lines changed

AUTHORS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,7 @@ The majority of the dotty codebase is new code, with the exception of the compon
7171
> the [compiler bridge in sbt 0.13](https://github.com/sbt/sbt/tree/0.13/compile/interface/src/main/scala/xsbt),
7272
> but has been heavily adapted and refactored.
7373
> Original authors were Mark Harrah, Grzegorz Kossakowski, Martin Duhemm, Adriaan Moors and others.
74+
75+
`dotty.tools.dotc.plugins`
76+
77+
> Adapted from [scala/scala](https://github.com/scala/scala) with some modifications. They were originally authored by Lex Spoon, Som Snytt, Adriaan Moors, Paul Phillips and others.

compiler/src/dotty/tools/dotc/Run.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,8 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
157157
if (ctx.settings.YtestPickler.value) List("pickler")
158158
else ctx.settings.YstopAfter.value
159159

160-
val phases = ctx.squashPhases(ctx.phasePlan,
160+
val pluginPlan = ctx.addPluginPhases(ctx.phasePlan)
161+
val phases = ctx.squashPhases(pluginPlan,
161162
ctx.settings.Yskip.value, ctx.settings.YstopBefore.value, stopAfter, ctx.settings.Ycheck.value)
162163
ctx.usePhases(phases)
163164

compiler/src/dotty/tools/dotc/config/CompilerCommand.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,15 @@ object CompilerCommand extends DotClass {
114114

115115
def shouldStopWithInfo = {
116116
import settings._
117-
Set(help, Xhelp, Yhelp) exists (_.value)
117+
Set(help, Xhelp, Yhelp, showPlugins) exists (_.value)
118118
}
119119

120120
def infoMessage: String = {
121121
import settings._
122122
if (help.value) usageMessage
123123
else if (Xhelp.value) xusageMessage
124124
else if (Yhelp.value) yusageMessage
125+
else if (showPlugins.value) ctx.pluginDescriptions
125126
else ""
126127
}
127128

compiler/src/dotty/tools/dotc/config/Printers.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ object Printers {
2828
val overload: Printer = noPrinter
2929
val patmatch: Printer = noPrinter
3030
val pickling: Printer = noPrinter
31+
val plugins: Printer = noPrinter
3132
val simplify: Printer = noPrinter
3233
val subtyping: Printer = noPrinter
3334
val transforms: Printer = noPrinter

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ class ScalaSettings extends Settings.SettingGroup {
5050
val printTasty = BooleanSetting("-print-tasty", "Prints the raw tasty when decompiling.")
5151
val printLines = BooleanSetting("-print-lines", "Show source code line numbers.")
5252

53+
/** Plugin-related setting */
54+
val plugin = MultiStringSetting ("-Xplugin", "paths", "Load a plugin from each classpath.")
55+
val disable = MultiStringSetting ("-Xplugin-disable", "plugin", "Disable plugins by name.")
56+
val require = MultiStringSetting ("-Xplugin-require", "plugin", "Abort if a named plugin is not loaded.")
57+
val showPlugins = BooleanSetting ("-Xplugin-list", "Print a synopsis of loaded plugins.")
58+
val pluginsDir = StringSetting ("-Xpluginsdir", "path", "Path to search for plugin archives.", Defaults.scalaPluginPath)
59+
val pluginOptions = MultiStringSetting ("-P", "plugin:opt", "Pass an option to a plugin, e.g. -P:<plugin>:<opt>")
60+
5361
/** -X "Advanced" settings
5462
*/
5563
val Xhelp = BooleanSetting("-X", "Print a synopsis of advanced options.")

compiler/src/dotty/tools/dotc/core/Contexts.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import dotty.tools.dotc.profile.Profiler
3535
import util.Property.Key
3636
import util.Store
3737
import xsbti.AnalysisCallback
38+
import plugins._
3839

3940
object Contexts {
4041

@@ -76,6 +77,7 @@ object Contexts {
7677
with SymDenotations
7778
with Reporting
7879
with NamerContextOps
80+
with Plugins
7981
with Cloneable { thiscontext =>
8082
implicit def ctx: Context = this
8183

compiler/src/dotty/tools/dotc/core/Phases.scala

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ object Phases {
8282
final def squashPhases(phasess: List[List[Phase]],
8383
phasesToSkip: List[String], stopBeforePhases: List[String], stopAfterPhases: List[String], YCheckAfter: List[String]): List[Phase] = {
8484
val squashedPhases = ListBuffer[Phase]()
85-
var prevPhases: Set[Class[_ <: Phase]] = Set.empty
85+
var prevPhases: Set[String] = Set.empty
8686
val YCheckAll = YCheckAfter.contains("all")
8787

8888
var stop = false
@@ -99,7 +99,6 @@ object Phases {
9999
val filteredPhaseBlock = filteredPhases(i)
100100
val phaseToAdd =
101101
if (filteredPhaseBlock.length > 1) {
102-
val phasesInBlock: Set[String] = filteredPhaseBlock.map(_.phaseName).toSet
103102
for (phase <- filteredPhaseBlock) {
104103
phase match {
105104
case p: MiniPhase =>
@@ -112,11 +111,11 @@ object Phases {
112111
}
113112
}
114113
val superPhase = new MegaPhase(filteredPhaseBlock.asInstanceOf[List[MiniPhase]].toArray)
115-
prevPhases ++= filteredPhaseBlock.map(_.getClazz)
114+
prevPhases ++= filteredPhaseBlock.map(_.phaseName)
116115
superPhase
117116
} else { // block of a single phase, no squashing
118117
val phase = filteredPhaseBlock.head
119-
prevPhases += phase.getClazz
118+
prevPhases += phase.phaseName
120119
phase
121120
}
122121
squashedPhases += phaseToAdd
@@ -147,7 +146,7 @@ object Phases {
147146

148147
phases = (NoPhase :: flatPhases.toList ::: new TerminalPhase :: Nil).toArray
149148
setSpecificPhases()
150-
var phasesAfter:Set[Class[_ <: Phase]] = Set.empty
149+
var phasesAfter: Set[String] = Set.empty
151150
nextDenotTransformerId = new Array[Int](phases.length)
152151
denotTransformers = new Array[DenotTransformer](phases.length)
153152

@@ -161,7 +160,7 @@ object Phases {
161160
val unmetPrecedeRequirements = p.runsAfter -- phasesAfter
162161
assert(unmetPrecedeRequirements.isEmpty,
163162
s"phase ${p} has unmet requirement: ${unmetPrecedeRequirements.mkString(", ")} should precede this phase")
164-
phasesAfter += p.getClazz
163+
phasesAfter += p.phaseName
165164

166165
}
167166
var i = 0
@@ -281,7 +280,7 @@ object Phases {
281280
def allowsImplicitSearch: Boolean = false
282281

283282
/** List of names of phases that should precede this phase */
284-
def runsAfter: Set[Class[_ <: Phase]] = Set.empty
283+
def runsAfter: Set[String] = Set.empty
285284

286285
/** @pre `isRunnable` returns true */
287286
def run(implicit ctx: Context): Unit
@@ -393,12 +392,4 @@ object Phases {
393392
def replace(oldPhaseClass: Class[_ <: Phase], newPhases: Phase => List[Phase], current: List[List[Phase]]): List[List[Phase]] =
394393
current.map(_.flatMap(phase =>
395394
if (oldPhaseClass.isInstance(phase)) newPhases(phase) else phase :: Nil))
396-
397-
/** Dotty deviation: getClass yields Class[_], instead of [Class <: <type of receiver>].
398-
* We can get back the old behavior using this decorator. We should also use the same
399-
* trick for standard getClass.
400-
*/
401-
private implicit class getClassDeco[T](val x: T) extends AnyVal {
402-
def getClazz: Class[_ <: T] = x.getClass.asInstanceOf[Class[_ <: T]]
403-
}
404395
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package dotty.tools.dotc
2+
package plugins
3+
4+
import core._
5+
import Contexts._
6+
import Phases._
7+
import dotty.tools.io._
8+
import transform.MegaPhase.MiniPhase
9+
10+
import java.io.InputStream
11+
import java.util.Properties
12+
13+
import scala.collection.mutable
14+
import scala.util.{ Try, Success, Failure }
15+
16+
trait PluginPhase extends MiniPhase {
17+
def runsBefore: Set[String] = Set.empty
18+
}
19+
20+
sealed trait Plugin {
21+
/** The name of this plugin */
22+
def name: String
23+
24+
/** A one-line description of the plugin */
25+
def description: String
26+
27+
/** Is this plugin a research plugin?
28+
*
29+
* Research plugin receives a phase plan and return a new phase plan, while
30+
* non-research plugin returns a list of phases to be inserted.
31+
*/
32+
def research: Boolean = isInstanceOf[ResearchPlugin]
33+
34+
/** A description of this plugin's options, suitable as a response
35+
* to the -help command-line option. Conventionally, the options
36+
* should be listed with the `-P:plugname:` part included.
37+
*/
38+
val optionsHelp: Option[String] = None
39+
}
40+
41+
trait StandardPlugin extends Plugin {
42+
/** Non-research plugins should override this method to return the phases
43+
*
44+
* @param options: commandline options to the plugin, `-P:plugname:opt1,opt2` becomes List(opt1, opt2)
45+
* @return a list of phases to be added to the phase plan
46+
*/
47+
def init(options: List[String]): List[PluginPhase]
48+
}
49+
50+
trait ResearchPlugin extends Plugin {
51+
/** Research plugins should override this method to return the new phase plan
52+
*
53+
* @param options: commandline options to the plugin, `-P:plugname:opt1,opt2` becomes List(opt1, opt2)
54+
* @param plan: the given phase plan
55+
* @return the new phase plan
56+
*/
57+
def init(options: List[String], plan: List[List[Phase]])(implicit ctx: Context): List[List[Phase]]
58+
}
59+
60+
object Plugin {
61+
62+
private val PluginFile = "plugin.properties"
63+
64+
/** Create a class loader with the specified locations plus
65+
* the loader that loaded the Scala compiler.
66+
*/
67+
private def loaderFor(locations: Seq[Path]): ClassLoader = {
68+
val compilerLoader = classOf[Plugin].getClassLoader
69+
val urls = locations map (_.toURL)
70+
71+
new java.net.URLClassLoader(urls.toArray, compilerLoader)
72+
}
73+
74+
type AnyClass = Class[_]
75+
76+
/** Use a class loader to load the plugin class.
77+
*/
78+
def load(classname: String, loader: ClassLoader): Try[AnyClass] = {
79+
import scala.util.control.NonFatal
80+
try {
81+
Success[AnyClass](loader loadClass classname)
82+
} catch {
83+
case NonFatal(e) =>
84+
Failure(new PluginLoadException(classname, s"Error: unable to load class $classname: ${e.getMessage}"))
85+
case e: NoClassDefFoundError =>
86+
Failure(new PluginLoadException(classname, s"Error: class not found: ${e.getMessage} required by $classname"))
87+
}
88+
}
89+
90+
/** Load all plugins specified by the arguments.
91+
* Each location of `paths` must be a valid plugin archive or exploded archive.
92+
* Each of `paths` must define one plugin.
93+
* Each of `dirs` may be a directory containing arbitrary plugin archives.
94+
* Skips all plugins named in `ignoring`.
95+
* A classloader is created to load each plugin.
96+
*/
97+
def loadAllFrom(
98+
paths: List[List[Path]],
99+
dirs: List[Path],
100+
ignoring: List[String]): List[Try[Plugin]] =
101+
{
102+
103+
def fromFile(inputStream: InputStream, path: Path): String = {
104+
val props = new Properties
105+
props.load(inputStream)
106+
107+
val pluginClass = props.getProperty("pluginClass")
108+
109+
if (pluginClass == null) throw new RuntimeException("Bad plugin descriptor: " + path)
110+
else pluginClass
111+
}
112+
113+
def loadDescriptionFromDir(f: Path): Try[String] = {
114+
val path = f / PluginFile
115+
Try(fromFile(new java.io.FileInputStream(path.jpath.toFile), path))
116+
}
117+
118+
def loadDescriptionFromJar(jarp: Path): Try[String] = {
119+
// XXX Return to this once we have more ARM support
120+
def read(is: InputStream) =
121+
if (is == null) throw new PluginLoadException(jarp.path, s"Missing $PluginFile in $jarp")
122+
else fromFile(is, jarp)
123+
124+
val fileEntry = new java.util.jar.JarEntry(PluginFile)
125+
Try(read(new Jar(jarp.jpath.toFile).getEntryStream(fileEntry)))
126+
}
127+
128+
// List[(jar, Try(descriptor))] in dir
129+
def scan(d: Directory) =
130+
d.files.toList sortBy (_.name) filter (Jar isJarOrZip _) map (j => (j, loadDescriptionFromJar(j)))
131+
132+
type PDResults = List[Try[(String, ClassLoader)]]
133+
134+
// scan plugin dirs for jars containing plugins, ignoring dirs with none and other jars
135+
val fromDirs: PDResults = dirs filter (_.isDirectory) flatMap { d =>
136+
scan(d.toDirectory) collect {
137+
case (j, Success(pd)) => Success((pd, loaderFor(Seq(j))))
138+
}
139+
}
140+
141+
// scan jar paths for plugins, taking the first plugin you find.
142+
// a path element can be either a plugin.jar or an exploded dir.
143+
def findDescriptor(ps: List[Path]) = {
144+
def loop(qs: List[Path]): Try[String] = qs match {
145+
case Nil => Failure(new MissingPluginException(ps))
146+
case p :: rest =>
147+
if (p.isDirectory) loadDescriptionFromDir(p.toDirectory) orElse loop(rest)
148+
else if (p.isFile) loadDescriptionFromJar(p.toFile) orElse loop(rest)
149+
else loop(rest)
150+
}
151+
loop(ps)
152+
}
153+
154+
val fromPaths: PDResults = paths map (p => findDescriptor(p) match {
155+
case Success(classname) => Success((classname, loaderFor(p)))
156+
case Failure(e) => Failure(e)
157+
})
158+
159+
val seen = mutable.HashSet[String]()
160+
val enabled = (fromPaths ::: fromDirs) map(_.flatMap {
161+
case (classname, loader) =>
162+
Plugin.load(classname, loader).flatMap { clazz =>
163+
val plugin = instantiate(clazz)
164+
if (seen(classname)) // a nod to scala/bug#7494, take the plugin classes distinctly
165+
Failure(new PluginLoadException(plugin.name, s"Ignoring duplicate plugin ${plugin.name} (${classname})"))
166+
else if (ignoring contains plugin.name)
167+
Failure(new PluginLoadException(plugin.name, s"Disabling plugin ${plugin.name}"))
168+
else {
169+
seen += classname
170+
Success(plugin)
171+
}
172+
}
173+
})
174+
enabled // distinct and not disabled
175+
}
176+
177+
/** Instantiate a plugin class, given the class and
178+
* the compiler it is to be used in.
179+
*/
180+
def instantiate(clazz: AnyClass): Plugin = clazz.newInstance.asInstanceOf[Plugin]
181+
}
182+
183+
class PluginLoadException(val path: String, message: String, cause: Exception) extends Exception(message, cause) {
184+
def this(path: String, message: String) = this(path, message, null)
185+
}
186+
187+
class MissingPluginException(path: String) extends PluginLoadException(path, s"No plugin in path $path") {
188+
def this(paths: List[Path]) = this(paths mkString File.pathSeparator)
189+
}

0 commit comments

Comments
 (0)