Skip to content

Commit ea05a3b

Browse files
committed
WIP - port plugin architecture from scalac
1 parent ba33ccb commit ea05a3b

File tree

8 files changed

+346
-3
lines changed

8 files changed

+346
-3
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ class Run(comp: Compiler, ictx: Context) {
112112
if (ctx.settings.YtestPickler.value) List("pickler")
113113
else ctx.settings.YstopAfter.value
114114

115-
val phases = ctx.squashPhases(ctx.phasePlan,
115+
val pluginPlan = ctx.plugins.foldRight(ctx.phasePlan) { (plug, plan) => plug.init(plan) }
116+
val phases = ctx.squashPhases(pluginPlan,
116117
ctx.settings.Yskip.value, ctx.settings.YstopBefore.value, stopAfter, ctx.settings.Ycheck.value)
117118
ctx.usePhases(phases)
118119

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

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

9797
def shouldStopWithInfo = {
9898
import settings._
99-
Set(help, Xhelp, Yhelp) exists (_.value)
99+
Set(help, Xhelp, Yhelp, showPlugins) exists (_.value)
100100
}
101101

102102
def infoMessage: String = {
103103
import settings._
104104
if (help.value) usageMessage
105105
else if (Xhelp.value) xusageMessage
106106
else if (Yhelp.value) yusageMessage
107+
else if (showPlugins.value) ctx.pluginDescriptions
107108
else ""
108109
}
109110

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ class ScalaSettings extends Settings.SettingGroup {
4444
val rewrite = OptionSetting[Rewrites]("-rewrite", "When used in conjunction with -language:Scala2 rewrites sources to migrate to new syntax")
4545
val silentWarnings = BooleanSetting("-nowarn", "Silence all warnings.")
4646

47+
/** Plugin-related setting */
48+
val plugin = MultiStringSetting ("-Xplugin", "paths", "Load a plugin from each classpath.")
49+
val disable = MultiStringSetting ("-Xplugin-disable", "plugin", "Disable plugins by name.")
50+
val require = MultiStringSetting ("-Xplugin-require", "plugin", "Abort if a named plugin is not loaded.")
51+
val showPlugins = BooleanSetting ("-Xplugin-list", "Print a synopsis of loaded plugins.")
52+
val pluginsDir = StringSetting ("-Xpluginsdir", "path", "Path to search for plugin archives.", Defaults.scalaPluginPath)
53+
val pluginOptions = MultiStringSetting ("-P", "plugin:opt", "Pass an option to a plugin, e.g. -P:<plugin>:<opt>")
54+
4755
/** -X "Advanced" settings
4856
*/
4957
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
@@ -33,6 +33,7 @@ import DenotTransformers.DenotTransformer
3333
import util.Property.Key
3434
import util.Store
3535
import xsbti.AnalysisCallback
36+
import plugins._
3637

3738
object Contexts {
3839

@@ -64,6 +65,7 @@ object Contexts {
6465
with SymDenotations
6566
with Reporting
6667
with NamerContextOps
68+
with Plugins
6769
with Cloneable { thiscontext =>
6870
implicit def ctx: Context = this
6971

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package dotty.tools.dotc
2+
package plugins
3+
4+
import core._
5+
import Contexts._
6+
import Phases._
7+
import dotty.tools.io._
8+
9+
import java.io.InputStream
10+
11+
import scala.collection.mutable
12+
import scala.util.{ Try, Success, Failure }
13+
14+
trait Plugin {
15+
/** The name of this plugin */
16+
def name: String
17+
18+
/** A one-line description of the plugin */
19+
def description: String
20+
21+
def options(implicit ctx: Context): List[String] = {
22+
// Process plugin options of form plugin:option
23+
def namec = name + ":"
24+
ctx.settings.pluginOptions.value filter (_ startsWith namec) map (_ stripPrefix namec)
25+
}
26+
27+
/** Handle any plugin-specific options.
28+
* The user writes `-P:plugname:opt1,opt2`,
29+
* but the plugin sees `List(opt1, opt2)`.
30+
* The plugin can opt out of further processing
31+
* by returning false. For example, if the plugin
32+
* has an "enable" flag, now would be a good time
33+
* to sit on the bench.
34+
* @param options plugin arguments
35+
* @param error error function
36+
* @return true to continue, or false to opt out
37+
*/
38+
def init(phases: List[List[Phase]])(implicit ctx: Context): List[List[Phase]] = phases
39+
40+
/** A description of this plugin's options, suitable as a response
41+
* to the -help command-line option. Conventionally, the options
42+
* should be listed with the `-P:plugname:` part included.
43+
*/
44+
val optionsHelp: Option[String] = None
45+
}
46+
47+
/** ...
48+
*
49+
* @author Lex Spoon
50+
* @version 1.0, 2007-5-21
51+
*/
52+
object Plugin {
53+
54+
private val PluginXML = "scalac-plugin.xml"
55+
56+
/** Create a class loader with the specified locations plus
57+
* the loader that loaded the Scala compiler.
58+
*/
59+
private def loaderFor(locations: Seq[Path]): ClassLoader = {
60+
val compilerLoader = classOf[Plugin].getClassLoader
61+
val urls = locations map (_.toURL)
62+
63+
new java.net.URLClassLoader(urls.toArray, compilerLoader)
64+
}
65+
66+
/** Try to load a plugin description from the specified location.
67+
*/
68+
private def loadDescriptionFromJar(jarp: Path): Try[PluginDescription] = {
69+
// XXX Return to this once we have more ARM support
70+
def read(is: InputStream) =
71+
if (is == null) throw new PluginLoadException(jarp.path, s"Missing $PluginXML in $jarp")
72+
else PluginDescription.fromXML(is)
73+
74+
val xmlEntry = new java.util.jar.JarEntry(PluginXML)
75+
Try(read(new Jar(jarp.jfile).getEntryStream(xmlEntry)))
76+
}
77+
78+
private def loadDescriptionFromFile(f: Path): Try[PluginDescription] =
79+
Try(PluginDescription.fromXML(new java.io.FileInputStream(f.jfile)))
80+
81+
type AnyClass = Class[_]
82+
83+
/** Use a class loader to load the plugin class.
84+
*/
85+
def load(classname: String, loader: ClassLoader): Try[AnyClass] = {
86+
import scala.util.control.NonFatal
87+
try {
88+
Success[AnyClass](loader loadClass classname)
89+
} catch {
90+
case NonFatal(e) =>
91+
Failure(new PluginLoadException(classname, s"Error: unable to load class: $classname"))
92+
case e: NoClassDefFoundError =>
93+
Failure(new PluginLoadException(classname, s"Error: class not found: ${e.getMessage} required by $classname"))
94+
}
95+
}
96+
97+
/** Load all plugins specified by the arguments.
98+
* Each location of `paths` must be a valid plugin archive or exploded archive.
99+
* Each of `paths` must define one plugin.
100+
* Each of `dirs` may be a directory containing arbitrary plugin archives.
101+
* Skips all plugins named in `ignoring`.
102+
* A classloader is created to load each plugin.
103+
*/
104+
def loadAllFrom(
105+
paths: List[List[Path]],
106+
dirs: List[Path],
107+
ignoring: List[String]): List[Try[AnyClass]] =
108+
{
109+
// List[(jar, Try(descriptor))] in dir
110+
def scan(d: Directory) =
111+
d.files.toList sortBy (_.name) filter (Jar isJarOrZip _) map (j => (j, loadDescriptionFromJar(j)))
112+
113+
type PDResults = List[Try[(PluginDescription, ClassLoader)]]
114+
115+
// scan plugin dirs for jars containing plugins, ignoring dirs with none and other jars
116+
val fromDirs: PDResults = dirs filter (_.isDirectory) flatMap { d =>
117+
scan(d.toDirectory) collect {
118+
case (j, Success(pd)) => Success((pd, loaderFor(Seq(j))))
119+
}
120+
}
121+
122+
// scan jar paths for plugins, taking the first plugin you find.
123+
// a path element can be either a plugin.jar or an exploded dir.
124+
def findDescriptor(ps: List[Path]) = {
125+
def loop(qs: List[Path]): Try[PluginDescription] = qs match {
126+
case Nil => Failure(new MissingPluginException(ps))
127+
case p :: rest =>
128+
if (p.isDirectory) loadDescriptionFromFile(p.toDirectory / PluginXML) orElse loop(rest)
129+
else if (p.isFile) loadDescriptionFromJar(p.toFile) orElse loop(rest)
130+
else loop(rest)
131+
}
132+
loop(ps)
133+
}
134+
val fromPaths: PDResults = paths map (p => (p, findDescriptor(p))) map {
135+
case (p, Success(pd)) => Success((pd, loaderFor(p)))
136+
case (_, Failure(e)) => Failure(e)
137+
}
138+
139+
val seen = mutable.HashSet[String]()
140+
val enabled = (fromPaths ::: fromDirs) map {
141+
case Success((pd, loader)) if seen(pd.classname) =>
142+
// a nod to scala/bug#7494, take the plugin classes distinctly
143+
Failure(new PluginLoadException(pd.name, s"Ignoring duplicate plugin ${pd.name} (${pd.classname})"))
144+
case Success((pd, loader)) if ignoring contains pd.name =>
145+
Failure(new PluginLoadException(pd.name, s"Disabling plugin ${pd.name}"))
146+
case Success((pd, loader)) =>
147+
seen += pd.classname
148+
Plugin.load(pd.classname, loader)
149+
case Failure(e) =>
150+
Failure(e)
151+
}
152+
enabled // distinct and not disabled
153+
}
154+
155+
/** Instantiate a plugin class, given the class and
156+
* the compiler it is to be used in.
157+
*/
158+
def instantiate(clazz: AnyClass): Plugin = clazz.newInstance.asInstanceOf[Plugin]
159+
}
160+
161+
class PluginLoadException(val path: String, message: String, cause: Exception) extends Exception(message, cause) {
162+
def this(path: String, message: String) = this(path, message, null)
163+
}
164+
165+
class MissingPluginException(path: String) extends PluginLoadException(path, s"No plugin in path $path") {
166+
def this(paths: List[Path]) = this(paths mkString File.pathSeparator)
167+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package dotty.tools.dotc
2+
package plugins
3+
4+
/** A description of a compiler plugin, suitable for serialization
5+
* to XML for inclusion in the plugin's .jar file.
6+
*
7+
* @author Lex Spoon
8+
* @version 1.0, 2007-5-21
9+
* @author Adriaan Moors
10+
* @version 2.0, 2013
11+
* @param name A short name of the plugin, used to identify it in
12+
* various contexts. The phase defined by the plugin
13+
* should have the same name.
14+
* @param classname The name of the main Plugin class.
15+
*/
16+
case class PluginDescription(name: String, classname: String) {
17+
/** An XML representation of this description.
18+
* It should be stored inside the jar archive file.
19+
*/
20+
def toXML: String =
21+
s"""|<plugin>
22+
| <name>${name}</name>
23+
| <classname>${classname}</classname>
24+
|</plugin>""".stripMargin
25+
}
26+
27+
/** Utilities for the PluginDescription class.
28+
*
29+
* @author Lex Spoon
30+
* @version 1.0, 2007-5-21
31+
* @author Adriaan Moors
32+
* @version 2.0, 2013
33+
*/
34+
object PluginDescription {
35+
private def text(ns: org.w3c.dom.NodeList): String =
36+
if (ns.getLength == 1) ns.item(0).getTextContent.trim
37+
else throw new RuntimeException("Bad plugin descriptor.")
38+
39+
def fromXML(xml: java.io.InputStream): PluginDescription = {
40+
import javax.xml.parsers.DocumentBuilderFactory
41+
val root = DocumentBuilderFactory.newInstance.newDocumentBuilder.parse(xml).getDocumentElement
42+
root.normalize()
43+
if (root.getNodeName != "plugin")
44+
throw new RuntimeException("Plugin descriptor root element must be <plugin>.")
45+
46+
PluginDescription(text(root.getElementsByTagName("name")), text(root.getElementsByTagName("classname")))
47+
}
48+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package dotty.tools.dotc
2+
package plugins
3+
4+
import core._
5+
import Contexts._
6+
import dotty.tools.dotc.config.PathResolver
7+
import dotty.tools.io._
8+
9+
/** Support for run-time loading of compiler plugins.
10+
*
11+
* @author Lex Spoon
12+
* @version 1.1, 2009/1/2
13+
* Updated 2009/1/2 by Anders Bach Nielsen: Added features to implement SIP 00002
14+
*/
15+
trait Plugins {
16+
self: Context =>
17+
18+
/** Load a rough list of the plugins. For speed, it
19+
* does not instantiate a compiler run. Therefore it cannot
20+
* test for same-named phases or other problems that are
21+
* filtered from the final list of plugins.
22+
*/
23+
protected def loadRoughPluginsList(implicit ctx: Context): List[Plugin] = {
24+
def asPath(p: String) = ClassPath split p
25+
val paths = ctx.settings.plugin.value filter (_ != "") map (s => asPath(s) map Path.apply)
26+
val dirs = {
27+
def injectDefault(s: String) = if (s.isEmpty) PathResolver.Defaults.scalaPluginPath else s
28+
asPath(ctx.settings.pluginsDir.value) map injectDefault map Path.apply
29+
}
30+
val maybes = Plugin.loadAllFrom(paths, dirs, ctx.settings.disable.value)
31+
val (goods, errors) = maybes partition (_.isSuccess)
32+
// Explicit parameterization of recover to avoid -Xlint warning about inferred Any
33+
errors foreach (_.recover[Any] {
34+
// legacy behavior ignores altogether, so at least warn devs
35+
case e: MissingPluginException => warning(e.getMessage)
36+
case e: Exception => inform(e.getMessage)
37+
})
38+
val classes = goods map (_.get) // flatten
39+
40+
// Each plugin must only be instantiated once. A common pattern
41+
// is to register annotation checkers during object construction, so
42+
// creating multiple plugin instances will leave behind stale checkers.
43+
classes map (Plugin.instantiate(_))
44+
}
45+
46+
private var _roughPluginsList: List[Plugin] = _
47+
protected def roughPluginsList(implicit ctx: Context): List[Plugin] =
48+
if (_roughPluginsList == null) {
49+
_roughPluginsList = loadRoughPluginsList
50+
_roughPluginsList
51+
}
52+
else _roughPluginsList
53+
54+
/** Load all available plugins. Skips plugins that
55+
* either have the same name as another one, or which
56+
* define a phase name that another one does.
57+
*/
58+
protected def loadPlugins(implicit ctx: Context): List[Plugin] = {
59+
// remove any with conflicting names or subcomponent names
60+
def pick(
61+
plugins: List[Plugin],
62+
plugNames: Set[String]): List[Plugin] =
63+
{
64+
if (plugins.isEmpty) return Nil // early return
65+
66+
val plug :: tail = plugins
67+
def withoutPlug = pick(tail, plugNames)
68+
def withPlug = plug :: pick(tail, plugNames + plug.name)
69+
70+
def note(msg: String): Unit = if (ctx.settings.verbose.value) inform(msg format plug.name)
71+
def fail(msg: String) = { note(msg) ; withoutPlug }
72+
73+
if (plugNames contains plug.name)
74+
fail("[skipping a repeated plugin: %s]")
75+
else if (ctx.settings.disable.value contains plug.name)
76+
fail("[disabling plugin: %s]")
77+
else {
78+
note("[loaded plugin %s]")
79+
withPlug
80+
}
81+
}
82+
83+
val plugs = pick(roughPluginsList, Set())
84+
85+
// Verify required plugins are present.
86+
for (req <- ctx.settings.require.value ; if !(plugs exists (_.name == req)))
87+
ctx.error("Missing required plugin: " + req)
88+
89+
// Verify no non-existent plugin given with -P
90+
for {
91+
opt <- ctx.settings.pluginOptions.value
92+
if !(plugs exists (opt startsWith _.name + ":"))
93+
} ctx.error("bad option: -P:" + opt)
94+
95+
plugs
96+
}
97+
98+
private var _plugins: List[Plugin] = _
99+
def plugins(implicit ctx: Context): List[Plugin] =
100+
if (_plugins == null) {
101+
_plugins = loadPlugins
102+
_plugins
103+
}
104+
else _plugins
105+
106+
107+
/** A description of all the plugins that are loaded */
108+
def pluginDescriptions: String =
109+
roughPluginsList map (x => "%s - %s".format(x.name, x.description)) mkString "\n"
110+
111+
/** Summary of the options for all loaded plugins */
112+
def pluginOptionsHelp: String =
113+
(for (plug <- roughPluginsList ; help <- plug.optionsHelp) yield {
114+
"\nOptions for plugin '%s':\n%s\n".format(plug.name, help)
115+
}).mkString
116+
}

0 commit comments

Comments
 (0)