mirror of https://github.com/sbt/sbt.git
Invalidate unmanagedFileStampCache in allOutputFiles
In the code formatting use case, the formatting task may modify the source files in place. If the formatting task uses the nio inputFileStamps, then it would fill the in-memory cache of source paths to file stamps. This would cause compile to see the pre-formatted stamps. To fix this, we can invalidate the file cache entries for the outputs of a task. This will cause the side-effect of some extra io because the hashes may be computed three times: once for the format inputs, once for the format outputs and once for the compile inputs. I think most users would understand that adding auto-formatting would potentially slowdown compilation. To really prove this out, I implemented a poor man's scalafmt plugin in a scripted test. It is fully incremental. Even in the case when some files cannot be formatted it will update all of the files that can be formatted and not re-format them until they change.
This commit is contained in:
parent
3f026972d5
commit
7c483909af
|
|
@ -348,7 +348,11 @@ private[sbt] object Settings {
|
|||
case LastModified => FileStamp.lastModified
|
||||
case Hash => FileStamp.hash
|
||||
}
|
||||
(allOutputFiles in scope).value.flatMap(p => stamper(p).map(p -> _))
|
||||
val allFiles = (allOutputFiles in scope).value
|
||||
// The cache invalidation is specifically so that source formatters can run before
|
||||
// the compile task and the file stamps seen by compile match the post-format stamps.
|
||||
allFiles.foreach((unmanagedFileStampCache in scope).value.invalidate)
|
||||
allFiles.flatMap(p => stamper(p).map(p -> _))
|
||||
})
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
version = 2.0.0
|
||||
maxColumn = 100
|
||||
project.git = true
|
||||
project.excludeFilters = [ "\\Wsbt-test\\W", "\\Winput_sources\\W", "\\Wcontraband-scala\\W" ]
|
||||
|
||||
# http://docs.scala-lang.org/style/scaladoc.html recommends the JavaDoc style.
|
||||
# scala/scala is written that way too https://github.com/scala/scala/blob/v2.12.2/src/library/scala/Predef.scala
|
||||
docstrings = JavaDoc
|
||||
|
||||
# This also seems more idiomatic to include whitespace in import x.{ yyy }
|
||||
spaces.inImportCurlyBraces = true
|
||||
|
||||
# This is more idiomatic Scala.
|
||||
# http://docs.scala-lang.org/style/indentation.html#methods-with-numerous-arguments
|
||||
align.openParenCallSite = false
|
||||
align.openParenDefnSite = false
|
||||
|
||||
# For better code clarity
|
||||
danglingParentheses = true
|
||||
|
||||
trailingCommas = preserve
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import java.nio.file.Path
|
||||
import complete.DefaultParsers._
|
||||
|
||||
enablePlugins(ScalafmtPlugin)
|
||||
|
||||
val classFiles = taskKey[Seq[Path]]("The classfiles generated by compile")
|
||||
classFiles := {
|
||||
val classes = (Compile / classDirectory).value.toGlob / ** / "*.class"
|
||||
fileTreeView.value.list(classes).map(_._1)
|
||||
}
|
||||
classFiles := classFiles.dependsOn(Compile / compile).value
|
||||
|
||||
val compileAndCheckNoClassFileUpdates = taskKey[Unit]("Checks that there are no class file updates")
|
||||
compileAndCheckNoClassFileUpdates := {
|
||||
val current = (classFiles / outputFileStamps).value.toSet
|
||||
val previous = (classFiles / outputFileStamps).previous.getOrElse(Nil).toSet
|
||||
assert(current == previous)
|
||||
}
|
||||
|
||||
val checkLastModified = inputKey[Unit]("Check the last modified time for a file")
|
||||
checkLastModified := {
|
||||
(Space ~> OptSpace ~> matched(charClass(_ != ' ').+) ~ (Space ~> ('!'.? ~ Digit.+.map(
|
||||
_.mkString.toLong
|
||||
)))).parsed match {
|
||||
case (file, (negate, expectedLastModified)) =>
|
||||
val sourceFile = baseDirectory.value / "src" / "main" / "scala" / file
|
||||
val lastModified = IO.getModifiedTimeOrZero(sourceFile)
|
||||
negate match {
|
||||
case Some(_) => assert(lastModified != expectedLastModified)
|
||||
case None => assert(lastModified == expectedLastModified)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val setLastModified = inputKey[Unit]("Set the last modified time for a file")
|
||||
setLastModified := {
|
||||
val Seq(file, lm) = Def.spaceDelimited().parsed
|
||||
val sourceFile = baseDirectory.value / "src" / "main" / "scala" / file
|
||||
IO.setModifiedTimeOrFalse(sourceFile, lm.toLong)
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
class Bar { val x = }
|
||||
|
|
@ -0,0 +1 @@
|
|||
class Bar {val x=2}
|
||||
|
|
@ -0,0 +1 @@
|
|||
class Foo{val x=1}
|
||||
|
|
@ -0,0 +1 @@
|
|||
libraryDependencies += "org.scalameta" %% "scalafmt-dynamic" % "2.0.0"
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import java.io.PrintWriter
|
||||
import java.nio.file._
|
||||
import sbt._
|
||||
import sbt.Keys.{ baseDirectory, unmanagedSources }
|
||||
import sbt.nio.Keys.{ fileInputs, inputFileStamps, outputFileStamper, outputFileStamps }
|
||||
import sbt.nio.FileStamper
|
||||
import org.scalafmt.interfaces.{ Scalafmt, ScalafmtReporter }
|
||||
|
||||
object ScalafmtPlugin extends AutoPlugin {
|
||||
private val reporter = new ScalafmtReporter {
|
||||
override def error(file: Path, message: String): Unit = throw new Exception(s"$file $message")
|
||||
override def error(file: Path, e: Throwable): Unit = throw e
|
||||
override def excluded(file: Path): Unit = {}
|
||||
override def parsedConfig(config: Path, scalafmtVersion: String): Unit = {}
|
||||
override def downloadWriter: PrintWriter = new PrintWriter(System.out, true)
|
||||
}
|
||||
private val formatter = Scalafmt.create(this.getClass.getClassLoader).withReporter(reporter)
|
||||
object autoImport {
|
||||
val scalafmtImpl = taskKey[Seq[Path]]("Format scala sources")
|
||||
val scalafmt = taskKey[Unit]("Format scala sources and validate results")
|
||||
}
|
||||
import autoImport._
|
||||
override lazy val projectSettings = super.projectSettings ++ Seq(
|
||||
Compile / scalafmtImpl / fileInputs := (Compile / unmanagedSources / fileInputs).value,
|
||||
Compile / scalafmtImpl / outputFileStamper := FileStamper.Hash,
|
||||
Compile / scalafmtImpl := {
|
||||
val config = baseDirectory.value.toPath / ".scalafmt.conf"
|
||||
val allInputStamps = (Compile / scalafmtImpl / inputFileStamps).value
|
||||
val previous =
|
||||
(Compile / scalafmtImpl / outputFileStamps).previous.map(_.toMap).getOrElse(Map.empty)
|
||||
allInputStamps.flatMap {
|
||||
case (p, s) if previous.get(p).fold(false)(_ == s) => Some(p)
|
||||
case (p, s) =>
|
||||
try {
|
||||
println(s"Formatting $p")
|
||||
Files.write(p, formatter.format(config, p, new String(Files.readAllBytes(p))).getBytes)
|
||||
Some(p)
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
println(e)
|
||||
None
|
||||
}
|
||||
}
|
||||
},
|
||||
Compile / scalafmt := {
|
||||
val outputs = (Compile / scalafmtImpl / outputFileStamps).value.toMap
|
||||
val improperlyFormatted = (Compile / scalafmtImpl).inputFiles.filterNot(outputs.contains _)
|
||||
if (improperlyFormatted.nonEmpty) {
|
||||
val msg = s"There were improperly formatted files:\n${improperlyFormatted mkString "\n"}"
|
||||
throw new IllegalStateException(msg)
|
||||
}
|
||||
},
|
||||
Compile / unmanagedSources / inputFileStamps :=
|
||||
(Compile / unmanagedSources / inputFileStamps).dependsOn(Compile / scalafmt).value
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
class Foo{val x=1}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
> setLastModified Foo.scala 12345678
|
||||
|
||||
# The first time we run compile, we expect an updated class file for Foo.class
|
||||
-> compileAndCheckNoClassFileUpdates
|
||||
|
||||
# scalafmt should modify Foo.scala
|
||||
> checkLastModified Foo.scala !12345678
|
||||
|
||||
# The first time we run compile, there should be no updates since Foo.scala hasn't changed since
|
||||
# scalafmt modified it in the first run
|
||||
> compileAndCheckNoClassFileUpdates
|
||||
|
||||
$ copy-file changes/Foo.scala src/main/scala/Foo.scala
|
||||
|
||||
$ copy-file changes/Bar-bad.scala src/main/scala/Bar.scala
|
||||
|
||||
> setLastModified Foo.scala 12345678
|
||||
|
||||
> setLastModified Bar.scala 12345678
|
||||
|
||||
# formatting should fail because Bar.scala is invalid, but Foo.scala should be re-formatted
|
||||
-> scalafmt
|
||||
|
||||
> checkLastModified Foo.scala !12345678
|
||||
|
||||
> checkLastModified Bar.scala 12345678
|
||||
|
||||
$ copy-file changes/Bar.scala src/main/scala/Bar.scala
|
||||
|
||||
> setLastModified Foo.scala 12345678
|
||||
|
||||
> setLastModified Bar.scala 12345678
|
||||
|
||||
# Formatting should now succeed and Foo.scala should not be re-formatted
|
||||
> scalafmt
|
||||
|
||||
> checkLastModified Foo.scala 12345678
|
||||
|
||||
> checkLastModified Bar.scala !12345678
|
||||
|
||||
# make sure that the custom clean task doesn't blow away the scala source files (it should exclude
|
||||
# any files not in the target directory
|
||||
> scalafmt / clean
|
||||
|
||||
$ exists src/main/scala/Foo.scala
|
||||
|
||||
$ exists src/main/scala/Bar.scala
|
||||
Loading…
Reference in New Issue