Skip to content

Commit 14f5f76

Browse files
committed
hooks for plugins to override Global.analyzer
As discussed a couple weeks ago, submitting this to formalize the hacks that I used in macro paradise for 2.10, so that it continues working after possible refactorings in 2.11. One of the strategies to implement this would be to make a list of functions overridden by the plugin and then to reuse the analyzer plugins architecture to introduce them as hooks. However: 1) The list is quite big and disparate (Namers.enterSym, Namers.ensureCompanionObject, Typers.reallyExists, Typers.typed1, Typers.typedTemplate, Typers.typedBlock, Typers.typedClassDef). 2) The list doesn't correspond to the spirit of analyzer plugins that modify return types of namer/typer functions rather than override them. 3) There's no guarantee that later on paradise won't need to override additional functionality of namer/typer, requiring new hacks. Another strategy lets plugins suggest their own analyzers to the compiler. It enables augmenting namer/typer with arbitrary functionality, providing ultimate flexibility. Shortcomings: 1) If multiple plugins want to install their own analyzers, it won't work (but such situations can be detected and meaningful errors can be reported). One can argue though, that even if we provided finer-grained API for such plugins, it wouldn't work anyway, because e.g. it's hard to imagine several plugins robustly applying their modifications to Namers.enterSym. None of the outlined two strategies is ideal, however, the second one makes more sense to me, because shortcoming #3 of the first one seems to defeat the idea of introducing the hooks into the compiler in the first place. Therefore this patch implements the second strategy.
1 parent 83e95c0 commit 14f5f76

20 files changed

+149
-23
lines changed

src/compiler/scala/tools/nsc/Global.scala

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -436,9 +436,28 @@ class Global(var currentSettings: Settings, var reporter: Reporter)
436436
// I only changed analyzer.
437437
//
438438
// factory for phases: namer, packageobjects, typer
439-
lazy val analyzer = new {
440-
val global: Global.this.type = Global.this
441-
} with Analyzer
439+
private var analyzerInitializing = false
440+
lazy val analyzer = {
441+
def defaultAnalyzer = new { val global: Global.this.type = Global.this } with Analyzer
442+
if (!analyzerInitializing) {
443+
analyzerInitializing = true
444+
val computedAnalyzer = {
445+
earlyPlugins.flatMap(_.analyzer) match {
446+
case Nil => defaultAnalyzer
447+
case analyzer :: Nil => analyzer
448+
case analyzer :: others =>
449+
val offenders = earlyPlugins.filter(_.analyzer.nonEmpty).map(_.name).mkString(", ")
450+
globalError(s"multiple plugins: $offenders attempt to replace the analyzer")
451+
defaultAnalyzer
452+
}
453+
}
454+
analyzerInitializing = false
455+
computedAnalyzer.asInstanceOf[Analyzer{ val global: Global.this.type }]
456+
} else {
457+
globalError(s"one of the plugins attempted to induce analyzer initialization loop")
458+
defaultAnalyzer
459+
}
460+
}
442461

443462
// phaseName = "patmat"
444463
object patmat extends {

src/compiler/scala/tools/nsc/plugins/Plugin.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package plugins
88

99
import scala.tools.nsc.io.{ Jar }
1010
import scala.tools.nsc.util.ScalaClassLoader
11+
import scala.tools.nsc.typechecker.Analyzer
1112
import scala.reflect.io.{ Directory, File, Path }
1213
import java.io.InputStream
1314
import java.util.zip.ZipException
@@ -56,6 +57,25 @@ abstract class Plugin {
5657
val optionsHelp: Option[String] = None
5758
}
5859

60+
/** Information about a plugin loaded from a jar file.
61+
*
62+
* In addition to normal functionality exposed by `Plugin`,
63+
* `EarlyPlugin` lets its users modify the very backbone of the compiler:
64+
* the `analyzer` component that spawns namers and typers.
65+
*
66+
* The concrete subclass must have a one-argument constructor
67+
* that accepts an instance of `global`.
68+
* {{{
69+
* (val global: Global)
70+
* }}}
71+
*/
72+
abstract class EarlyPlugin extends Plugin {
73+
/** The custom analyzer that this plugin requires, if any.
74+
* If multiple plugins require custom analyzers, an error is raised.
75+
*/
76+
val analyzer: Option[Analyzer] = None
77+
}
78+
5979
/** ...
6080
*
6181
* @author Lex Spoon
@@ -145,3 +165,9 @@ object Plugin {
145165
(clazz getConstructor classOf[Global] newInstance global).asInstanceOf[Plugin]
146166
}
147167
}
168+
169+
object EarlyPlugin {
170+
def instantiate(clazz: Class[_], global: Global): EarlyPlugin = {
171+
Plugin.instantiate(clazz, global).asInstanceOf[EarlyPlugin]
172+
}
173+
}

src/compiler/scala/tools/nsc/plugins/Plugins.scala

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,7 @@ import scala.tools.util.PathResolver.Defaults
1919
trait Plugins {
2020
self: Global =>
2121

22-
/** Load a rough list of the plugins. For speed, it
23-
* does not instantiate a compiler run. Therefore it cannot
24-
* test for same-named phases or other problems that are
25-
* filtered from the final list of plugins.
26-
*/
27-
protected def loadRoughPluginsList(): List[Plugin] = {
22+
protected lazy val (earlyPluginClasses, latePluginClasses) = {
2823
val jars = settings.plugin.value map Path.apply
2924
def injectDefault(s: String) = if (s.isEmpty) Defaults.scalaPluginPath else s
3025
val dirs = (settings.pluginsDir.value split File.pathSeparator).toList map injectDefault map Path.apply
@@ -33,14 +28,21 @@ trait Plugins {
3328
// Explicit parameterization of recover to suppress -Xlint warning about inferred Any
3429
errors foreach (_.recover[Any] { case e: Exception => inform(e.getMessage) })
3530
val classes = goods map (_.get) // flatten
36-
37-
// Each plugin must only be instantiated once. A common pattern
38-
// is to register annotation checkers during object construction, so
39-
// creating multiple plugin instances will leave behind stale checkers.
40-
classes map (Plugin.instantiate(_, this))
31+
classes.partition(classOf[EarlyPlugin].isAssignableFrom(_))
4132
}
4233

43-
protected lazy val roughPluginsList: List[Plugin] = loadRoughPluginsList()
34+
// Each plugin must only be instantiated once. A common pattern
35+
// is to register annotation checkers during object construction, so
36+
// creating multiple plugin instances will leave behind stale checkers.
37+
lazy val earlyPlugins = earlyPluginClasses map (EarlyPlugin.instantiate(_, this))
38+
protected lazy val latePlugins: List[Plugin] = latePluginClasses map (Plugin.instantiate(_, this))
39+
40+
/** Load a rough list of the plugins. For speed, it
41+
* does not instantiate a compiler run. Therefore it cannot
42+
* test for same-named phases or other problems that are
43+
* filtered from the final list of plugins.
44+
*/
45+
protected lazy val roughPluginsList: List[Plugin] = earlyPlugins ++ latePlugins
4446

4547
/** Load all available plugins. Skips plugins that
4648
* either have the same name as another one, or which

src/compiler/scala/tools/nsc/typechecker/Analyzer.scala

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ trait Analyzer extends AnyRef
2929
val global : Global
3030
import global._
3131

32-
object namerFactory extends {
32+
lazy val namerFactory = new NamerFactory
33+
34+
class NamerFactory extends SubComponent {
3335
val global: Analyzer.this.global.type = Analyzer.this.global
34-
} with SubComponent {
3536
val phaseName = "namer"
3637
val runsAfter = List[String]("parser")
3738
val runsRightAfter = None
@@ -45,9 +46,10 @@ trait Analyzer extends AnyRef
4546
}
4647
}
4748

48-
object packageObjects extends {
49+
lazy val packageObjects = new PackageObjects
50+
51+
class PackageObjects extends SubComponent {
4952
val global: Analyzer.this.global.type = Analyzer.this.global
50-
} with SubComponent {
5153
val phaseName = "packageobjects"
5254
val runsAfter = List[String]()
5355
val runsRightAfter= Some("namer")
@@ -73,10 +75,11 @@ trait Analyzer extends AnyRef
7375
}
7476
}
7577

76-
object typerFactory extends {
77-
val global: Analyzer.this.global.type = Analyzer.this.global
78-
} with SubComponent {
78+
lazy val typerFactory = new TyperFactory
79+
80+
class TyperFactory extends SubComponent {
7981
import scala.reflect.internal.TypesStats.typerNanos
82+
val global: Analyzer.this.global.type = Analyzer.this.global
8083
val phaseName = "typer"
8184
val runsAfter = List[String]()
8285
val runsRightAfter = Some("packageobjects")

src/partest/scala/tools/partest/nest/FileManager.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ trait FileManager extends FileUtil {
158158
val xprefix = "-Xplugin:"
159159
val (xplugs, others) = args partition (_ startsWith xprefix)
160160
val Xplugin = if (xplugs.isEmpty) Nil else List(xprefix +
161-
(xplugs map (_ stripPrefix xprefix) flatMap (_ split pathSeparator) map absolutize mkString pathSeparator)
161+
(xplugs map (_ stripPrefix xprefix) flatMap (_ split ",") map absolutize mkString ",")
162162
)
163163
SCALAC_OPTS.toList ::: others ::: Xplugin
164164
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
error: multiple plugins: analyzerReplacingPlugin1, analyzerReplacingPlugin2 attempt to replace the analyzer
2+
one error found
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-Xplugin:files/plugins/analyzerReplacingPlugin1.jar,files/plugins/analyzerReplacingPlugin2.jar
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
object Test extends App {
2+
println("hello")
3+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
error: one of the plugins attempted to induce analyzer initialization loop
2+
one error found
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package analyzerReplacingPlugins
2+
3+
import scala.tools.nsc.{ Global, Phase }
4+
import scala.tools.nsc.plugins.{ EarlyPlugin, PluginComponent }
5+
import scala.tools.nsc.typechecker.Analyzer
6+
import scala.reflect.io.Path
7+
import scala.reflect.io.File
8+
import scala.reflect.internal.Mode
9+
10+
/** A test plugin. */
11+
class Ploogin(val global: Global) extends EarlyPlugin {
12+
import global._
13+
14+
val name = "analyzerReplacingPlugin"
15+
val description = "A sample analyzer replacing plugin for testing."
16+
val components = Nil
17+
override val analyzer: Option[Analyzer] = Some(global.analyzer)
18+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-Xplugin:.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
object Test extends App {
2+
println("hello")
3+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<plugin>
2+
<name>analyzer-replacing-plugin</name>
3+
<classname>analyzerReplacingPlugins.Ploogin</classname>
4+
</plugin>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
7ecf05c5d5e9038462b5cee3f4a753a42e108ed4 ?analyzerReplacingPlugin1.jar
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
7e02a93333895a56fb807676bc496856e23e5a97 ?analyzerReplacingPlugin2.jar
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello world
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package analyzerReplacingPlugins
2+
3+
import scala.tools.nsc.{ Global, Phase }
4+
import scala.tools.nsc.plugins.{ EarlyPlugin, PluginComponent }
5+
import scala.tools.nsc.typechecker.Analyzer
6+
import scala.reflect.io.Path
7+
import scala.reflect.io.File
8+
import scala.reflect.internal.Mode
9+
10+
/** A test plugin. */
11+
class Ploogin(val global: Global) extends EarlyPlugin {
12+
import global._
13+
14+
val name = "analyzerReplacingPlugin"
15+
val description = "A sample analyzer replacing plugin for testing."
16+
val components = Nil
17+
override val analyzer: Option[Analyzer] = Some(new { val global: Ploogin.this.global.type = Ploogin.this.global } with MyAnalyzer)
18+
19+
private trait MyAnalyzer extends Analyzer {
20+
val global: Ploogin.this.global.type
21+
override def newTyper(context: Context) = new Typer(context) {
22+
override def typed1(tree: Tree, mode: Mode, pt: Type): Tree = {
23+
val tree1 = tree match {
24+
case tree @ Literal(Constant("hello")) if tree.tpe == null => Literal(Constant("hello world"))
25+
case _ => tree
26+
}
27+
super.typed1(tree1, mode, pt)
28+
}
29+
}
30+
}
31+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-Xplugin:.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
object Test extends App {
2+
println("hello")
3+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<plugin>
2+
<name>analyzer-replacing-plugin</name>
3+
<classname>analyzerReplacingPlugins.Ploogin</classname>
4+
</plugin>

0 commit comments

Comments
 (0)