diff --git a/.scalafmt.conf b/.scalafmt.conf index c8f8c35..3993380 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -19,3 +19,9 @@ newlines.source = keep runner.dialect = scala213 +fileOverride { + "glob:**/src-3/**.scala" { + runner.dialect = scala3 + rewrite.scala3.removeOptionalBraces.enabled = false + } +} diff --git a/build.sc b/build.sc index 6fa90ff..296636d 100755 --- a/build.sc +++ b/build.sc @@ -7,7 +7,7 @@ import mill.eval.Evaluator import mill.define.{SelectMode, Command} object Settings { - val version = "0.10.9" + val version = "0.10.10" val pomOrg = "com.lihaoyi" val githubOrg = "com-lihaoyi" val githubRepo = "mill-moduledefs" @@ -15,9 +15,12 @@ object Settings { } object Deps { - val scalaVersions = 0.to(14).map(v => "2.13." + v) - val scalaVersion = scalaVersions.reverse.head - def scalaCompiler(scalaVersion: String) = ivy"org.scala-lang:scala-compiler:${scalaVersion}" + val scala2Versions = 0.to(14).map(v => "2.13." + v) + val scala3Versions = Seq("3.5.0") + val scalaAllVersions = Map(scala2Versions.last -> scala2Versions, scala3Versions.last -> scala3Versions) + def scalaCompiler(scalaVersion: String) = + if (scalaVersion.startsWith("3.")) ivy"org.scala-lang::scala3-compiler:${scalaVersion}" + else ivy"org.scala-lang:scala-compiler:${scalaVersion}" val sourcecode = ivy"com.lihaoyi::sourcecode:0.3.0" } @@ -35,26 +38,43 @@ trait ModuledefsBase extends ScalaModule with PublishModule { ) ) override def javacOptions = Seq("-source", "1.8", "-target", "1.8", "-encoding", "UTF-8") + override def scalacOptions = T { + super.scalacOptions() ++ ( + if (scalaVersion().startsWith("3.")) Seq("-Yexplicit-nulls", "-no-indent") + else Seq.empty + ) + } } -object moduledefs extends ModuledefsBase { +object moduledefs extends Cross[ModuleDefsCross](Deps.scalaAllVersions.keys.toSeq: _*) +class ModuleDefsCross(override val crossScalaVersion: String) extends CrossScalaModule + with ModuledefsBase { outer => override def artifactName = "mill-" + super.artifactName() - override def scalaVersion = Deps.scalaVersion - override def ivyDeps = Agg( - Deps.scalaCompiler(scalaVersion()), - Deps.sourcecode - ) + override def ivyDeps = { + val sv = crossScalaVersion + Agg(Deps.sourcecode) ++ + (if (sv.startsWith("2.")) Agg(Deps.scalaCompiler(sv)) else Agg.empty) + } - object plugin extends Cross[PluginCross](Deps.scalaVersions: _*) + object plugin extends Cross[PluginCross](Deps.scalaAllVersions(crossScalaVersion): _*) class PluginCross(override val crossScalaVersion: String) extends CrossScalaModule with ModuledefsBase { - override def artifactName = "scalac-mill-" + super.artifactName() - override def moduleDeps = Seq(moduledefs) + override def artifactName = "scalac-mill-moduledefs-plugin" + // ^^ TODO: cant use `"scalac-mill-" + super.artifactName()` here + // because it includes the crossScalaVersion of `moduledefs` + // could be addressed with Cross2 from mill 0.11.x + override def moduleDeps = Seq(moduledefs(outer.crossScalaVersion)) override def crossFullScalaVersion = true override def ivyDeps = Agg( - Deps.scalaCompiler(scalaVersion()), + Deps.scalaCompiler(crossScalaVersion), Deps.sourcecode ) + + override def resources = T.sources { + super.resources() ++ scalaVersionDirectoryNames.map(dir => + PathRef(millSourcePath / s"resources-$dir") + ) + } } } diff --git a/moduledefs/plugin/resources/scalac-plugin.xml b/moduledefs/plugin/resources-2/scalac-plugin.xml similarity index 100% rename from moduledefs/plugin/resources/scalac-plugin.xml rename to moduledefs/plugin/resources-2/scalac-plugin.xml diff --git a/moduledefs/plugin/resources-3/plugin.properties b/moduledefs/plugin/resources-3/plugin.properties new file mode 100644 index 0000000..a7d492e --- /dev/null +++ b/moduledefs/plugin/resources-3/plugin.properties @@ -0,0 +1 @@ +pluginClass=mill.moduledefs.AutoOverridePluginDotty diff --git a/moduledefs/plugin/src/AutoOverridePlugin.scala b/moduledefs/plugin/src-2/AutoOverridePlugin.scala similarity index 100% rename from moduledefs/plugin/src/AutoOverridePlugin.scala rename to moduledefs/plugin/src-2/AutoOverridePlugin.scala diff --git a/moduledefs/plugin/src-3/AutoOverridePluginDotty.scala b/moduledefs/plugin/src-3/AutoOverridePluginDotty.scala new file mode 100644 index 0000000..ad81004 --- /dev/null +++ b/moduledefs/plugin/src-3/AutoOverridePluginDotty.scala @@ -0,0 +1,113 @@ +package mill.moduledefs + +import scala.collection.mutable.ListBuffer + +import dotty.tools.dotc.plugins.{StandardPlugin, PluginPhase} +import dotty.tools.dotc.core.Contexts.{Context, ctx} +import dotty.tools.dotc.core.Comments.docCtx +import dotty.tools.dotc.core.Symbols.Symbol +import dotty.tools.dotc.core.Symbols.ClassSymbol +import dotty.tools.dotc.core.Symbols.requiredClass +import dotty.tools.dotc.core.Names.termName +import dotty.tools.dotc.core.Flags +import dotty.tools.dotc.report +import dotty.tools.dotc.core.Constants.Constant +import dotty.tools.dotc.core.Annotations.Annotation +import dotty.tools.dotc.ast.tpd.{Template, ValDef, DefDef, Literal, NamedArg} +import dotty.tools.dotc.util.Spans.Span + +class AutoOverridePluginDotty extends StandardPlugin { + + override def initialize(options: List[String])(using Context): List[PluginPhase] = + List(EnableScaladocAnnotation(), AutoOverride()) + + val name = "auto-override-plugin" + val description = "automatically inserts `override` keywords for you" + + /** basically we override each kind of definition to copy over its doc comment to an annotation. */ + private class EnableScaladocAnnotation extends PluginPhase { + + override val phaseName: String = "EmbedScaladocAnnotation" + + override val runsAfter: Set[String] = Set("posttyper") + override val runsBefore: Set[String] = Set("pickler") // TODO: should the annotation be in TASTY? + + private var _ScalaDocAnnot: ClassSymbol | Null = null + private def ScalaDocAnnot(using Context): ClassSymbol = { + val local = _ScalaDocAnnot + if local == null then { + val sym = requiredClass(AutoOverridePluginDotty.scaladocAnnotationClassName) + _ScalaDocAnnot = sym + sym + } else local + } + private lazy val valueName = termName("value") + + private def cookComment(sym: Symbol, span: Span)(using Context): Unit = { + for + docCtx <- ctx.docCtx + comment <- docCtx.docstring(sym) + do { + val text = NamedArg(valueName, Literal(Constant(comment.raw))).withSpan(span) + sym.addAnnotation(Annotation(ScalaDocAnnot, text, span)) + } + } + + override def prepareForTemplate(tree: Template)(using Context): Context = { + cookComment(tree.symbol, tree.span) + ctx + } + + override def prepareForValDef(tree: ValDef)(using Context): Context = { + cookComment(tree.symbol, tree.span) + ctx + } + + override def prepareForDefDef(tree: DefDef)(using Context): Context = { + cookComment(tree.symbol, tree.span) + ctx + } + + } + + /** This phase automatically adds the override annotation to methods that require one. */ + private class AutoOverride extends PluginPhase { + + override val runsAfter = Set("posttyper") + override val runsBefore = Set("crossVersionChecks") // this is where override checking happens + + val phaseName = "auto-override" + + private var _Cacher: ClassSymbol | Null = null + private def Cacher(using Context): ClassSymbol = { + val local = _Cacher + if local == null then { + val sym = requiredClass(AutoOverridePluginDotty.cacherClassName) + _Cacher = sym + sym + } else local + } + + private def isCacher(owner: Symbol)(using Context): Boolean = + if owner.isClass then owner.asClass.baseClasses.exists(_ == Cacher) + else false + + override def prepareForDefDef(d: DefDef)(using Context): Context = { + val sym = d.symbol + if sym.allOverriddenSymbols.count(!_.is(Flags.Abstract)) >= 1 + && !sym.is(Flags.Override) + && isCacher(sym.owner) + then + sym.flags = sym.flags | Flags.Override + ctx + } + } + +} + +object AutoOverridePluginDotty { + + private val cacherClassName = "mill.moduledefs.Cacher" + private val scaladocAnnotationClassName = "mill.moduledefs.Scaladoc" + +} diff --git a/moduledefs/src/Cacher.scala b/moduledefs/src-2/Cacher.scala similarity index 100% rename from moduledefs/src/Cacher.scala rename to moduledefs/src-2/Cacher.scala diff --git a/moduledefs/src-3/Cacher.scala b/moduledefs/src-3/Cacher.scala new file mode 100644 index 0000000..1bb1a11 --- /dev/null +++ b/moduledefs/src-3/Cacher.scala @@ -0,0 +1,58 @@ +package mill.moduledefs + +import scala.collection.mutable +import scala.quoted.* + +trait Cacher { + private lazy val cacherLazyMap = mutable.Map.empty[sourcecode.Enclosing, Any] + + protected def cachedTarget[T](t: => T)(implicit c: sourcecode.Enclosing): T = synchronized { + cacherLazyMap.getOrElseUpdate(c, t).asInstanceOf[T] + } +} + +object Cacher { + private def withMacroOwner[T](using Quotes)(op: quotes.reflect.Symbol => T): T = { + import quotes.reflect.* + + // In Scala 3, the top level splice of a macro is owned by a symbol called "macro" with the macro flag set, + // but not the method flag. + def isMacroOwner(sym: Symbol)(using Quotes): Boolean = + sym.name == "macro" && sym.flags.is(Flags.Macro | Flags.Synthetic) && !sym.flags.is( + Flags.Method + ) + + def loop(owner: Symbol): T = + if owner.isPackageDef || owner == Symbol.noSymbol then + report.errorAndAbort( + "Cannot find the owner of the macro expansion", + Position.ofMacroExpansion + ) + else if isMacroOwner(owner) then op(owner.owner) // Skip the "macro" owner + else loop(owner.owner) + + loop(Symbol.spliceOwner) + } + + def impl0[T: Type](using Quotes)(t: Expr[T]): Expr[T] = withMacroOwner { owner => + import quotes.reflect.* + + val CacherSym = TypeRepr.of[Cacher].typeSymbol + + val ownerIsCacherClass = + owner.owner.isClassDef && + owner.owner.typeRef.baseClasses.contains(CacherSym) + + if (ownerIsCacherClass && owner.flags.is(Flags.Method)) { + val enclosingCtx = Expr.summon[sourcecode.Enclosing].getOrElse( + report.errorAndAbort("Cannot find enclosing context", Position.ofMacroExpansion) + ) + + val thisSel = This(owner.owner).asExprOf[Cacher] + '{ $thisSel.cachedTarget[T](${ t })(using $enclosingCtx) } + } else report.errorAndAbort( + "T{} members must be defs defined in a Cacher class/trait/object body", + Position.ofMacroExpansion + ) + } +}