Skip to content

Commit

Permalink
Scala 3 support (#136)
Browse files Browse the repository at this point in the history
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
lihaoyi authored Sep 23, 2024
2 parents 50c2faf + 23c947d commit 014f9ab
Show file tree
Hide file tree
Showing 19 changed files with 644 additions and 222 deletions.
1 change: 1 addition & 0 deletions acyclic/resources/plugin.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pluginClass=acyclic.plugin.RuntimePlugin
File renamed without changes.
68 changes: 68 additions & 0 deletions acyclic/src-2/acyclic/plugin/PluginPhase.scala
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"
}
}
5 changes: 5 additions & 0 deletions acyclic/src-3/acyclic/plugin/Compat.scala
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 acyclic/src-3/acyclic/plugin/DependencyExtraction.scala
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
}
}
39 changes: 39 additions & 0 deletions acyclic/src-3/acyclic/plugin/Plugin.scala
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())
}
}
81 changes: 81 additions & 0 deletions acyclic/src-3/acyclic/plugin/PluginPhase.scala
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()
}
2 changes: 1 addition & 1 deletion acyclic/src/acyclic/package.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import scala.reflect.internal.annotations.compileTimeOnly
import scala.annotation.compileTimeOnly
package object acyclic {

/**
Expand Down
Loading

0 comments on commit 014f9ab

Please sign in to comment.