Fix todos in actions/compile-clean

This commit is contained in:
Adrien Piquerez 2024-04-08 16:27:35 +02:00
parent e1cf43c6bd
commit ab1aa6d001
7 changed files with 135 additions and 146 deletions

View File

@ -12,70 +12,54 @@ import java.io.File
import scala.quoted.*
import scala.reflect.ClassTag
import sbt.util.OptJsonWriter
import sbt.util.{ NoJsonWriter, OptJsonWriter }
import sbt.internal.util.{ AttributeKey, KeyTag }
private[sbt] object KeyMacro:
def settingKeyImpl[A1: Type](
description: Expr[String]
)(using qctx: Quotes): Expr[SettingKey[A1]] =
keyImpl2[A1, SettingKey[A1]]("settingKey") { (name, mf, ojw) =>
val n = Expr(name)
'{
SettingKey[A1]($n, $description)($mf, $ojw)
}
}
def settingKeyImpl[A1: Type](description: Expr[String])(using Quotes): Expr[SettingKey[A1]] =
val name = definingValName(errorMsg("settingKey"))
val tag = '{ KeyTag.Setting[A1](${ summonRuntimeClass[A1] }) }
val ojw = Expr
.summon[OptJsonWriter[A1]]
.getOrElse(errorAndAbort(s"OptJsonWriter[A] not found for ${Type.show[A1]}"))
'{ SettingKey(AttributeKey($name, $description, Int.MaxValue)(using $tag, $ojw)) }
def taskKeyImpl[A1: Type](description: Expr[String])(using qctx: Quotes): Expr[TaskKey[A1]] =
keyImpl[A1, TaskKey[A1]]("taskKey") { (name, mf) =>
val n = Expr(name)
'{
TaskKey[A1]($n, $description)($mf)
}
}
def taskKeyImpl[A1: Type](description: Expr[String])(using Quotes): Expr[TaskKey[A1]] =
val name = definingValName(errorMsg("taskKey"))
val tag: Expr[KeyTag[Task[A1]]] = Type.of[A1] match
case '[Seq[a]] =>
'{ KeyTag.SeqTask(${ summonRuntimeClass[a] }) }
case _ => '{ KeyTag.Task(${ summonRuntimeClass[A1] }) }
'{ TaskKey(AttributeKey($name, $description, Int.MaxValue)(using $tag, NoJsonWriter())) }
def inputKeyImpl[A1: Type](description: Expr[String])(using qctx: Quotes): Expr[InputKey[A1]] =
keyImpl[A1, InputKey[A1]]("inputKey") { (name, mf) =>
val n = Expr(name)
'{
InputKey[A1]($n, $description)($mf)
}
}
def inputKeyImpl[A1: Type](description: Expr[String])(using Quotes): Expr[InputKey[A1]] =
val name = definingValName(errorMsg("inputTaskKey"))
val tag: Expr[KeyTag[InputTask[A1]]] = '{ KeyTag.InputTask(${ summonRuntimeClass[A1] }) }
'{ InputKey(AttributeKey($name, $description, Int.MaxValue)(using $tag, NoJsonWriter())) }
private def keyImpl[A1: Type, A2: Type](methodName: String)(
f: (String, Expr[ClassTag[A1]]) => Expr[A2]
)(using qctx: Quotes): Expr[A2] =
val tpe = summon[Type[A1]]
f(
definingValName(errorMsg(methodName)),
Expr.summon[ClassTag[A1]].getOrElse(sys.error("ClassTag[A] not found for $tpe"))
)
def projectImpl(using Quotes): Expr[Project] =
val name = definingValName(errorMsg2)
'{ Project($name, new File($name)) }
private def keyImpl2[A1: Type, A2: Type](methodName: String)(
f: (String, Expr[ClassTag[A1]], Expr[OptJsonWriter[A1]]) => Expr[A2]
)(using qctx: Quotes): Expr[A2] =
val tpe = summon[Type[A1]]
f(
definingValName(errorMsg(methodName)),
Expr.summon[ClassTag[A1]].getOrElse(sys.error("ClassTag[A] not found for $tpe")),
Expr.summon[OptJsonWriter[A1]].getOrElse(sys.error("OptJsonWriter[A] not found for $tpe")),
)
private def summonRuntimeClass[A: Type](using Quotes): Expr[Class[?]] =
val classTag = Expr
.summon[ClassTag[A]]
.getOrElse(errorAndAbort(s"ClassTag[${Type.show[A]}] not found"))
'{ $classTag.runtimeClass }
def projectImpl(using qctx: Quotes): Expr[Project] =
val name = Expr(definingValName(errorMsg2("project")))
'{
Project($name, new File($name))
}
private def errorAndAbort(msg: String)(using q: Quotes): Nothing =
q.reflect.report.errorAndAbort(msg)
private def errorMsg(methodName: String): String =
s"""$methodName must be directly assigned to a val, such as `val x = $methodName[Int]("description")`."""
private def errorMsg2(methodName: String): String =
s"""$methodName must be directly assigned to a val, such as `val x = ($methodName in file("core"))`."""
private def errorMsg2: String =
"""project must be directly assigned to a val, such as `val x = project.in(file("core"))`."""
private def definingValName(errorMsg: String)(using qctx: Quotes): String =
private def definingValName(errorMsg: String)(using qctx: Quotes): Expr[String] =
val term = enclosingTerm
if term.isValDef then term.name
else sys.error(errorMsg)
if term.isValDef then Expr(term.name)
else errorAndAbort(errorMsg)
def enclosingTerm(using qctx: Quotes) =
import qctx.reflect._

View File

@ -21,6 +21,7 @@ final case class ScopedKeyData[A](scoped: ScopedKey[A], value: Any) {
def description: String =
key.tag match
case KeyTag.Task(typeArg) => s"Task: $typeArg"
case KeyTag.SeqTask(typeArg) => s"Task: Seq[$typeArg]"
case KeyTag.InputTask(typeArg) => s"Input task: $typeArg"
case KeyTag.Setting(typeArg) => s"Setting: $typeArg = $value"
}

View File

@ -24,6 +24,7 @@ import sbt.nio.file.Glob.{ GlobOps }
import sbt.util.Level
import sjsonnew.JsonFormat
import scala.annotation.nowarn
import xsbti.{ PathBasedFile, VirtualFileRef }
private[sbt] object Clean {
@ -142,8 +143,13 @@ private[sbt] object Clean {
private[sbt] object ToSeqPath:
given identitySeqPath: ToSeqPath[Seq[Path]] = identity[Seq[Path]](_)
given seqFile: ToSeqPath[Seq[File]] = _.map(_.toPath)
given virtualFileRefSeq: ToSeqPath[Seq[VirtualFileRef]] =
_.collect { case f: PathBasedFile => f.toPath }
given path: ToSeqPath[Path] = _ :: Nil
given file: ToSeqPath[File] = _.toPath :: Nil
given virtualFileRef: ToSeqPath[VirtualFileRef] =
case f: PathBasedFile => Seq(f.toPath)
case _ => Nil
end ToSeqPath
private[this] implicit class ToSeqPathOps[T](val t: T) extends AnyVal {

View File

@ -16,6 +16,7 @@ import sbt.io.IO
import sbt.nio.file.FileAttributes
import sjsonnew.{ Builder, JsonFormat, Unbuilder, deserializationError }
import xsbti.compile.analysis.{ Stamp => XStamp }
import xsbti.VirtualFileRef
/**
* A trait that indicates what file stamping implementation should be used to track the state of
@ -102,65 +103,49 @@ object FileStamp {
private[sbt] final case class Error(exception: IOException) extends FileStamp
object Formats {
implicit val seqPathJsonFormatter: JsonFormat[Seq[Path]] = new JsonFormat[Seq[Path]] {
override def write[J](obj: Seq[Path], builder: Builder[J]): Unit = {
builder.beginArray()
obj.foreach { path =>
builder.writeString(path.toString)
implicit val seqPathJsonFormatter: JsonFormat[Seq[Path]] =
asStringArray(_.toString, Paths.get(_))
implicit val seqFileJsonFormatter: JsonFormat[Seq[File]] =
asStringArray(_.toString, new File(_))
implicit val seqVirtualFileRefJsonFormatter: JsonFormat[Seq[VirtualFileRef]] =
asStringArray(_.id, VirtualFileRef.of)
implicit val fileJsonFormatter: JsonFormat[File] = fromSeqJsonFormat[File]
implicit val pathJsonFormatter: JsonFormat[Path] = fromSeqJsonFormat[Path]
implicit val virtualFileRefJsonFormatter: JsonFormat[VirtualFileRef] =
fromSeqJsonFormat[VirtualFileRef]
private def asStringArray[T](toStr: T => String, fromStr: String => T): JsonFormat[Seq[T]] =
new JsonFormat[Seq[T]] {
override def write[J](obj: Seq[T], builder: Builder[J]): Unit = {
builder.beginArray()
obj.foreach { x => builder.writeString(toStr(x)) }
builder.endArray()
}
builder.endArray()
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): Seq[T] =
jsOpt match {
case Some(js) =>
val size = unbuilder.beginArray(js)
val res = (1 to size) map { _ =>
fromStr(unbuilder.readString(unbuilder.nextElement))
}
unbuilder.endArray()
res
case None =>
deserializationError("Expected JsArray but found None")
}
}
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): Seq[Path] =
jsOpt match {
case Some(js) =>
val size = unbuilder.beginArray(js)
val res = (1 to size) map { _ =>
Paths.get(unbuilder.readString(unbuilder.nextElement))
}
unbuilder.endArray()
res
case None =>
deserializationError("Expected JsArray but found None")
}
}
private def fromSeqJsonFormat[T](using seqJsonFormat: JsonFormat[Seq[T]]): JsonFormat[T] =
new JsonFormat[T] {
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): T =
seqJsonFormat.read(jsOpt, unbuilder).head
implicit val seqFileJsonFormatter: JsonFormat[Seq[File]] = new JsonFormat[Seq[File]] {
override def write[J](obj: Seq[File], builder: Builder[J]): Unit = {
builder.beginArray()
obj.foreach { file =>
builder.writeString(file.toString)
}
builder.endArray()
override def write[J](obj: T, builder: Builder[J]): Unit =
seqJsonFormat.write(obj :: Nil, builder)
}
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): Seq[File] =
jsOpt match {
case Some(js) =>
val size = unbuilder.beginArray(js)
val res = (1 to size) map { _ =>
new File(unbuilder.readString(unbuilder.nextElement))
}
unbuilder.endArray()
res
case None =>
deserializationError("Expected JsArray but found None")
}
}
implicit val fileJsonFormatter: JsonFormat[File] = new JsonFormat[File] {
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): File =
seqFileJsonFormatter.read(jsOpt, unbuilder).head
override def write[J](obj: File, builder: Builder[J]): Unit =
seqFileJsonFormatter.write(obj :: Nil, builder)
}
implicit val pathJsonFormatter: JsonFormat[Path] = new JsonFormat[Path] {
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): Path =
seqPathJsonFormatter.read(jsOpt, unbuilder).head
override def write[J](obj: Path, builder: Builder[J]): Unit =
seqPathJsonFormatter.write(obj :: Nil, builder)
}
implicit val seqPathFileStampJsonFormatter: JsonFormat[Seq[(Path, FileStamp)]] =
new JsonFormat[Seq[(Path, FileStamp)]] {
override def write[J](obj: Seq[(Path, FileStamp)], builder: Builder[J]): Unit = {

View File

@ -25,6 +25,8 @@ import sjsonnew.JsonFormat
import scala.annotation.nowarn
import scala.collection.immutable.VectorBuilder
import java.io.File
import xsbti.VirtualFileRef
private[sbt] object Settings {
private[sbt] def inject(transformed: Seq[Def.Setting[_]]): Seq[Def.Setting[_]] = {
@ -68,45 +70,55 @@ private[sbt] object Settings {
setting: Def.Setting[_],
fileOutputScopes: Set[Scope]
): List[Def.Setting[_]] = {
val attributeKey = setting.key.key
attributeKey.tag match {
setting.key.key.tag match {
case tag: KeyTag.Task[?] =>
def default: List[Def.Setting[_]] = {
val scope = setting.key.scope.copy(task = Select(attributeKey))
if (fileOutputScopes.contains(scope)) {
val sk = setting.asInstanceOf[Def.Setting[Task[Any]]].key
val scopedKey = Keys.dynamicFileOutputs in (sk.scope in sk.key)
addTaskDefinition {
val init: Def.Initialize[Task[Seq[Path]]] = sk(_.map(_ => Nil))
Def.setting[Task[Seq[Path]]](scopedKey, init, setting.pos)
} :: allOutputPathsImpl(scope) :: outputFileStampsImpl(scope) :: cleanImpl(scope) :: Nil
} else Nil
}
def mkSetting[T: JsonFormat: ToSeqPath]: List[Def.Setting[_]] = {
val sk = setting.asInstanceOf[Def.Setting[Task[T]]].key
val taskKey = TaskKey(sk.key) in sk.scope
// We create a previous reference so that clean automatically works without the
// user having to explicitly call previous anywhere.
val init = Previous.runtime(taskKey).zip(taskKey) { case (_, t) =>
t.map(implicitly[ToSeqPath[T]].apply)
}
val key = Def.ScopedKey(taskKey.scope in taskKey.key, Keys.dynamicFileOutputs.key)
addTaskDefinition(Def.setting[Task[Seq[Path]]](key, init, setting.pos)) ::
outputsAndStamps(taskKey)
}
if seqClass.isAssignableFrom(tag.typeArg) then
// TODO fix this: maybe using the taskKey macro to convey the information
// t.typeArguments match {
// case p :: Nil if pathClass.isAssignableFrom(p.runtimeClass) => mkSetting[Seq[Path]]
// case _ => default
// }
default
else if pathClass.isAssignableFrom(tag.typeArg) then mkSetting[Path]
else default
if pathClass.isAssignableFrom(tag.typeArg) then addOutputAndStampTasks[Path](setting)
else if fileClass.isAssignableFrom(tag.typeArg) then addOutputAndStampTasks[File](setting)
else if virtualFileRefClass.isAssignableFrom(tag.typeArg) then
addOutputAndStampTasks[VirtualFileRef](setting)
else addDefaultTasks(setting, fileOutputScopes)
case tag: KeyTag.SeqTask[?] =>
if pathClass.isAssignableFrom(tag.typeArg) then addOutputAndStampTasks[Seq[Path]](setting)
else if fileClass.isAssignableFrom(tag.typeArg) then
addOutputAndStampTasks[Seq[File]](setting)
else if virtualFileRefClass.isAssignableFrom(tag.typeArg) then
addOutputAndStampTasks[Seq[VirtualFileRef]](setting)
else addDefaultTasks(setting, fileOutputScopes)
case _ => Nil
}
}
@nowarn
private def addDefaultTasks(
setting: Def.Setting[_],
fileOutputScopes: Set[Scope]
): List[Def.Setting[_]] = {
val scope = setting.key.scope.copy(task = Select(setting.key.key))
if (fileOutputScopes.contains(scope)) {
val sk = setting.asInstanceOf[Def.Setting[Task[Any]]].key
val scopedKey = Keys.dynamicFileOutputs in (sk.scope in sk.key)
val init: Def.Initialize[Task[Seq[Path]]] = sk(_.map(_ => Nil))
addTaskDefinition(Def.setting[Task[Seq[Path]]](scopedKey, init, setting.pos)) ::
allOutputPathsImpl(scope) :: outputFileStampsImpl(scope) :: cleanImpl(scope) :: Nil
} else Nil
}
@nowarn
private def addOutputAndStampTasks[T: JsonFormat: ToSeqPath](
setting: Def.Setting[_]
): List[Def.Setting[_]] = {
val sk = setting.asInstanceOf[Def.Setting[Task[T]]].key
val taskKey = TaskKey(sk.key) in sk.scope
// We create a previous reference so that clean automatically works without the
// user having to explicitly call previous anywhere.
val init = Previous.runtime(taskKey).zip(taskKey) { case (_, t) =>
t.map(implicitly[ToSeqPath[T]].apply)
}
val key = Def.ScopedKey(taskKey.scope in taskKey.key, Keys.dynamicFileOutputs.key)
addTaskDefinition(Def.setting[Task[Seq[Path]]](key, init, setting.pos)) ::
outputsAndStamps(taskKey)
}
private[sbt] val inject: Def.ScopedKey[_] => Seq[Def.Setting[_]] = scopedKey =>
scopedKey.key match {
case transitiveDynamicInputs.key =>
@ -161,7 +173,9 @@ private[sbt] object Settings {
}
private[this] val seqClass = classOf[Seq[_]]
private[this] val pathClass = classOf[java.nio.file.Path]
private[this] val pathClass = classOf[Path]
private val fileClass = classOf[File]
private val virtualFileRefClass = classOf[VirtualFileRef]
/**
* Returns all of the paths for the regular files described by a glob. Directories and hidden

View File

@ -6,17 +6,14 @@ $ exists target/out/jvm/scala-2.12.17/compile-clean/test-classes/B.class
> Test/clean
$ exists target/cant-touch-this
# TODO it should clean only test classes
# $ exists target/out/jvm/scala-2.12.17/compile-clean/classes/A.class
# $ exists target/out/jvm/scala-2.12.17/compile-clean/classes/X.class
$ exists target/out/jvm/scala-2.12.17/compile-clean/classes/A.class
$ exists target/out/jvm/scala-2.12.17/compile-clean/classes/X.class
$ absent target/out/jvm/scala-2.12.17/compile-clean/test-classes/B.class
# compiling everything again, but now cleaning only compile classes
> Test/products
> Compile/clean
$ exists target/cant-touch-this
# TODO it should clean only compile classes
$ absent target/out/jvm/scala-2.12.17/compile-clean/classes/A.class
# $ exists target/out/jvm/scala-2.12.17/compile-clean/test-classes/B.class
# TODO and X has to be kept, because of the cleanKeepFiles override
# $ exists target/out/jvm/scala-2.12.17/compile-clean/classes/X.class
$ exists target/out/jvm/scala-2.12.17/compile-clean/test-classes/B.class
$ exists target/out/jvm/scala-2.12.17/compile-clean/classes/X.class

View File

@ -15,11 +15,13 @@ import sjsonnew.*
enum KeyTag[A]:
case Setting[A](typeArg: Class[?]) extends KeyTag[A]
case Task[A](typeArg: Class[?]) extends KeyTag[A]
case SeqTask[A](typeArg: Class[?]) extends KeyTag[A]
case InputTask[A](typeArg: Class[?]) extends KeyTag[A]
override def toString: String = this match
case Setting(typeArg) => typeArg.toString
case Task(typeArg) => s"Task[$typeArg]"
case SeqTask(typeArg) => s"Task[Seq[$typeArg]]"
case InputTask(typeArg) => s"InputTask[$typeArg]"
def typeArg: Class[?]