-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Closes #76 Adds support for Scala 3.3.{0,1,2,3}, Scala 3.4.{0,1,2,3}, and Scala 3.5.0. I aimed for as close as a 1-to-1 port as possible, especially in `PluginPhase.scala` and `DependencyExtraction.scala` where the meat of the logic lives. I'm happy to clarify/elaborate on any specific code
- Loading branch information
Showing
19 changed files
with
644 additions
and
222 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pluginClass=acyclic.plugin.RuntimePlugin |
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package acyclic.plugin | ||
|
||
import acyclic.file | ||
import acyclic.plugin.Compat._ | ||
import scala.collection.{SortedSet, mutable} | ||
import scala.tools.nsc.{Global, Phase} | ||
import tools.nsc.plugins.PluginComponent | ||
|
||
/** | ||
* - Break dependency graph into strongly connected components | ||
* - Turn acyclic packages into virtual "files" in the dependency graph, as | ||
* aggregates of all the files within them | ||
* - Any strongly connected component which includes an acyclic.file or | ||
* acyclic.pkg is a failure | ||
* - Pick an arbitrary cycle and report it | ||
* - Don't report more than one cycle per file/pkg, to avoid excessive spam | ||
*/ | ||
class PluginPhase( | ||
val global: Global, | ||
cycleReporter: Seq[(Value, SortedSet[Int])] => Unit, | ||
force: => Boolean, | ||
fatal: => Boolean | ||
) extends PluginComponent { t => | ||
|
||
import global._ | ||
|
||
val runsAfter = List("typer") | ||
|
||
override val runsBefore = List("patmat") | ||
|
||
val phaseName = "acyclic" | ||
|
||
private object base extends BasePluginPhase[CompilationUnit, Tree, Symbol] with GraphAnalysis[Tree] { | ||
protected val cycleReporter = t.cycleReporter | ||
protected lazy val force = t.force | ||
protected lazy val fatal = t.fatal | ||
|
||
def treeLine(tree: Tree): Int = tree.pos.line | ||
def treeSymbolString(tree: Tree): String = tree.symbol.toString | ||
|
||
def reportError(msg: String): Unit = global.error(msg) | ||
def reportWarning(msg: String): Unit = global.warning(msg) | ||
def reportInform(msg: String): Unit = global.inform(msg) | ||
def reportEcho(msg: String, tree: Tree): Unit = global.reporter.echo(tree.pos, msg) | ||
|
||
def units: Seq[CompilationUnit] = global.currentRun.units.toSeq.sortBy(_.source.content.mkString.hashCode()) | ||
def unitTree(unit: CompilationUnit): Tree = unit.body | ||
def unitPath(unit: CompilationUnit): String = unit.source.path | ||
def unitPkgName(unit: CompilationUnit): List[String] = | ||
unit.body.collect { case x: PackageDef => x.pid.toString }.flatMap(_.split('.')) | ||
def findPkgObjects(tree: Tree): List[Tree] = tree.collect { case x: ModuleDef if x.name.toString == "package" => x } | ||
def pkgObjectName(pkgObject: Tree): String = pkgObject.symbol.enclosingPackageClass.fullName | ||
def hasAcyclicImport(tree: Tree, selector: String): Boolean = | ||
tree.collect { | ||
case Import(expr, List(sel)) => expr.symbol.toString == "package acyclic" && sel.name.toString == selector | ||
}.exists(identity) | ||
|
||
def extractDependencies(unit: CompilationUnit): Seq[(Symbol, Tree)] = DependencyExtraction(global)(unit) | ||
def symbolPath(sym: Symbol): String = sym.sourceFile.path | ||
def isValidSymbol(sym: Symbol): Boolean = sym != NoSymbol && sym.sourceFile != null | ||
} | ||
|
||
override def newPhase(prev: Phase): Phase = new Phase(prev) { | ||
override def run(): Unit = base.runAllUnits() | ||
|
||
def name: String = "acyclic" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package acyclic.plugin | ||
|
||
import acyclic.file | ||
|
||
object Compat |
100 changes: 100 additions & 0 deletions
100
acyclic/src-3/acyclic/plugin/DependencyExtraction.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
package acyclic.plugin | ||
|
||
import acyclic.file | ||
import dotty.tools.dotc.ast.tpd | ||
import dotty.tools.dotc.{CompilationUnit, report} | ||
import dotty.tools.dotc.core.Contexts.Context | ||
import dotty.tools.dotc.core.Flags | ||
import dotty.tools.dotc.core.Names.Name | ||
import dotty.tools.dotc.core.Symbols.Symbol | ||
import dotty.tools.dotc.core.Types.Type | ||
|
||
object DependencyExtraction { | ||
def apply(unit: CompilationUnit)(using Context): Seq[(Symbol, tpd.Tree)] = { | ||
|
||
class CollectTypeTraverser[T](pf: PartialFunction[Type, T]) extends tpd.TreeAccumulator[List[T]] { | ||
def apply(acc: List[T], tree: tpd.Tree)(using Context) = | ||
foldOver( | ||
if (pf.isDefinedAt(tree.tpe)) pf(tree.tpe) :: acc else acc, | ||
tree | ||
) | ||
} | ||
|
||
abstract class ExtractDependenciesTraverser extends tpd.TreeTraverser { | ||
protected val depBuf = collection.mutable.ArrayBuffer.empty[(Symbol, tpd.Tree)] | ||
protected def addDependency(sym: Symbol, tree: tpd.Tree): Unit = depBuf += ((sym, tree)) | ||
def dependencies: collection.immutable.Set[(Symbol, tpd.Tree)] = { | ||
// convert to immutable set and remove NoSymbol if we have one | ||
depBuf.toSet | ||
} | ||
|
||
} | ||
|
||
class ExtractDependenciesByMemberRefTraverser extends ExtractDependenciesTraverser { | ||
override def traverse(tree: tpd.Tree)(using Context): Unit = { | ||
tree match { | ||
case i @ tpd.Import(expr, selectors) => | ||
selectors.foreach { s => | ||
def lookupImported(name: Name) = expr.symbol.info.member(name).symbol | ||
|
||
if (s.isWildcard) { | ||
addDependency(lookupImported(s.name.toTermName), tree) | ||
addDependency(lookupImported(s.name.toTypeName), tree) | ||
} | ||
} | ||
case select: tpd.Select => | ||
addDependency(select.symbol, tree) | ||
/* | ||
* Idents are used in number of situations: | ||
* - to refer to local variable | ||
* - to refer to a top-level package (other packages are nested selections) | ||
* - to refer to a term defined in the same package as an enclosing class; | ||
* this looks fishy, see this thread: | ||
* https://groups.google.com/d/topic/scala-internals/Ms9WUAtokLo/discussion | ||
*/ | ||
case ident: tpd.Ident => | ||
addDependency(ident.symbol, tree) | ||
case typeTree: tpd.TypeTree => | ||
val typeSymbolCollector = new CollectTypeTraverser({ | ||
case tpe if tpe != null && tpe.typeSymbol != null && !tpe.typeSymbol.is(Flags.Package) => tpe.typeSymbol | ||
}) | ||
val deps = typeSymbolCollector(Nil, typeTree).toSet | ||
deps.foreach(addDependency(_, tree)) | ||
case t: tpd.Template => | ||
traverse(t.body) | ||
case other => () | ||
} | ||
foldOver((), tree) | ||
} | ||
} | ||
|
||
def byMembers(): collection.immutable.Set[(Symbol, tpd.Tree)] = { | ||
val traverser = new ExtractDependenciesByMemberRefTraverser | ||
if (!unit.isJava) | ||
traverser.traverse(unit.tpdTree) | ||
traverser.dependencies | ||
} | ||
|
||
class ExtractDependenciesByInheritanceTraverser extends ExtractDependenciesTraverser { | ||
override def traverse(tree: tpd.Tree)(using Context): Unit = tree match { | ||
case t: tpd.Template => | ||
// we are using typeSymbol and not typeSymbolDirect because we want | ||
// type aliases to be expanded | ||
val parentTypeSymbols = t.parents.map(parent => parent.tpe.typeSymbol).toSet | ||
report.debuglog("Parent type symbols for " + tree.sourcePos.show + ": " + parentTypeSymbols.map(_.fullName)) | ||
parentTypeSymbols.foreach(addDependency(_, tree)) | ||
traverse(t.body) | ||
case tree => foldOver((), tree) | ||
} | ||
} | ||
|
||
def byInheritence(): collection.immutable.Set[(Symbol, tpd.Tree)] = { | ||
val traverser = new ExtractDependenciesByInheritanceTraverser | ||
if (!unit.isJava) | ||
traverser.traverse(unit.tpdTree) | ||
traverser.dependencies | ||
} | ||
|
||
(byMembers() | byInheritence()).toSeq | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package acyclic.plugin | ||
|
||
import acyclic.file | ||
import dotty.tools.dotc.plugins.{PluginPhase, StandardPlugin} | ||
import scala.collection.SortedSet | ||
import dotty.tools.dotc.core.Contexts.Context | ||
|
||
class RuntimePlugin extends TestPlugin() | ||
class TestPlugin(cycleReporter: Seq[(Value, SortedSet[Int])] => Unit = _ => ()) extends StandardPlugin { | ||
|
||
val name = "acyclic" | ||
val description = "Allows the developer to prohibit inter-file dependencies" | ||
|
||
var force = false | ||
var fatal = true | ||
var alreadyRun = false | ||
|
||
private class Phase() extends PluginPhase { | ||
val phaseName = "acyclic" | ||
override val runsBefore = Set("patternMatcher") | ||
|
||
override def run(using Context): Unit = { | ||
if (!alreadyRun) { | ||
alreadyRun = true | ||
new acyclic.plugin.PluginPhase(cycleReporter, force, fatal).run() | ||
} | ||
} | ||
} | ||
|
||
override def init(options: List[String]): List[PluginPhase] = { | ||
if (options.contains("force")) { | ||
force = true | ||
} | ||
if (options.contains("warn")) { | ||
fatal = false | ||
} | ||
List(Phase()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package acyclic.plugin | ||
|
||
import acyclic.file | ||
import scala.collection.SortedSet | ||
import dotty.tools.dotc.ast.tpd | ||
import dotty.tools.dotc.{CompilationUnit, report} | ||
import dotty.tools.dotc.core.Contexts.Context | ||
import dotty.tools.dotc.core.Symbols.{NoSymbol, Symbol} | ||
import dotty.tools.dotc.util.NoSource | ||
|
||
/** | ||
* - Break dependency graph into strongly connected components | ||
* - Turn acyclic packages into virtual "files" in the dependency graph, as | ||
* aggregates of all the files within them | ||
* - Any strongly connected component which includes an acyclic.file or | ||
* acyclic.pkg is a failure | ||
* - Pick an arbitrary cycle and report it | ||
* - Don't report more than one cycle per file/pkg, to avoid excessive spam | ||
*/ | ||
class PluginPhase( | ||
protected val cycleReporter: Seq[(Value, SortedSet[Int])] => Unit, | ||
protected val force: Boolean, | ||
protected val fatal: Boolean | ||
)(using ctx: Context) extends BasePluginPhase[CompilationUnit, tpd.Tree, Symbol], GraphAnalysis[tpd.Tree] { | ||
|
||
def treeLine(tree: tpd.Tree): Int = tree.sourcePos.line + 1 | ||
def treeSymbolString(tree: tpd.Tree): String = tree.symbol.toString | ||
|
||
def reportError(msg: String): Unit = report.error(msg) | ||
def reportWarning(msg: String): Unit = report.warning(msg) | ||
def reportInform(msg: String): Unit = report.echo(msg) | ||
def reportEcho(msg: String, tree: tpd.Tree): Unit = report.echo(msg, tree.srcPos) | ||
|
||
private val pkgNameAccumulator = new tpd.TreeAccumulator[List[String]] { | ||
@annotation.tailrec | ||
private def definitivePackageDef(pkg: tpd.PackageDef): tpd.PackageDef = | ||
pkg.stats.collectFirst { case p: tpd.PackageDef => p } match { | ||
case Some(p) => definitivePackageDef(p) | ||
case None => pkg | ||
} | ||
|
||
def apply(acc: List[String], tree: tpd.Tree)(using Context) = tree match { | ||
case p: tpd.PackageDef => definitivePackageDef(p).pid.show :: acc | ||
case _ => foldOver(acc, tree) | ||
} | ||
} | ||
|
||
private val pkgObjectAccumulator = new tpd.TreeAccumulator[List[tpd.Tree]] { | ||
def apply(acc: List[tpd.Tree], tree: tpd.Tree)(using Context): List[tpd.Tree] = | ||
foldOver( | ||
if (tree.symbol.isPackageObject) tree :: acc else acc, | ||
tree | ||
) | ||
} | ||
|
||
private def hasAcyclicImportAccumulator(selector: String) = new tpd.TreeAccumulator[Boolean] { | ||
def apply(acc: Boolean, tree: tpd.Tree)(using Context): Boolean = tree match { | ||
case tpd.Import(expr, List(sel)) => | ||
acc || (expr.symbol.toString == "object acyclic" && sel.name.show == selector) | ||
case _ => foldOver(acc, tree) | ||
} | ||
} | ||
|
||
lazy val units = Option(ctx.run) match { | ||
case Some(run) => run.units.toSeq.sortBy(_.source.content.mkString.hashCode()) | ||
case None => Seq() | ||
} | ||
|
||
def unitTree(unit: CompilationUnit): tpd.Tree = unit.tpdTree | ||
def unitPath(unit: CompilationUnit): String = unit.source.path | ||
def unitPkgName(unit: CompilationUnit): List[String] = pkgNameAccumulator(Nil, unit.tpdTree).reverse.flatMap(_.split('.')) | ||
def findPkgObjects(tree: tpd.Tree): List[tpd.Tree] = pkgObjectAccumulator(Nil, tree).reverse | ||
def pkgObjectName(pkgObject: tpd.Tree): String = pkgObject.symbol.enclosingPackageClass.fullName.toString | ||
def hasAcyclicImport(tree: tpd.Tree, selector: String): Boolean = hasAcyclicImportAccumulator(selector)(false, tree) | ||
|
||
def extractDependencies(unit: CompilationUnit): Seq[(Symbol, tpd.Tree)] = DependencyExtraction(unit) | ||
def symbolPath(sym: Symbol): String = sym.source.path | ||
def isValidSymbol(sym: Symbol): Boolean = sym != NoSymbol && sym.source != null && sym.source != NoSource | ||
|
||
def run(): Unit = runAllUnits() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.