Skip to content

Commit b1bc003

Browse files
opt-in caching for scalafix invocations
1 parent ad913b9 commit b1bc003

File tree

17 files changed

+383
-25
lines changed

17 files changed

+383
-25
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package sbt.internal.sbtscalafix
2+
3+
import java.io.ByteArrayOutputStream
4+
5+
import sbinary._
6+
import sbt._
7+
import scalafix.internal.sbt.Arg
8+
9+
import scala.util.DynamicVariable
10+
11+
object Caching {
12+
13+
trait CacheKeysStamper
14+
extends InputCache[Seq[Arg.CacheKey]]
15+
with CacheImplicits {
16+
private val output = new DynamicVariable[Output](null)
17+
18+
implicit val lastModifiedFormat = FileInfo.lastModified.format
19+
val baFormat = implicitly[Format[Array[Byte]]]
20+
val baEquiv = implicitly[Equiv[Array[Byte]]]
21+
22+
protected def stamp: Arg.CacheKey => Unit
23+
24+
final def write[T](t: T)(implicit format: Format[T]): Unit =
25+
format.writes(output.value, t)
26+
27+
override final type Internal = Array[Byte]
28+
override final def convert(keys: Seq[Arg.CacheKey]): Array[Byte] = {
29+
val baos = new ByteArrayOutputStream()
30+
output.withValue(new JavaOutput(baos)) {
31+
keys.foreach(stamp)
32+
}
33+
Hash.apply(baos.toByteArray)
34+
}
35+
override final def read(from: Input): Array[Byte] =
36+
baFormat.reads(from)
37+
override final def write(to: Output, ba: Array[Byte]): Unit =
38+
baFormat.writes(to, ba)
39+
override final def equiv: Equiv[Array[Byte]] =
40+
baEquiv
41+
}
42+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package sbt.internal.sbtscalafix
2+
3+
import scalafix.internal.sbt.Arg
4+
import sjsonnew._
5+
6+
import scala.util.DynamicVariable
7+
8+
object Caching {
9+
10+
trait CacheKeysStamper
11+
extends JsonFormat[Seq[Arg.CacheKey]]
12+
with BasicJsonProtocol {
13+
private val builder = new DynamicVariable[Builder[_]](null)
14+
15+
protected def stamp: Arg.CacheKey => Unit
16+
17+
final def write[T](t: T)(implicit format: JsonFormat[T]): Unit =
18+
format.write(t, builder.value)
19+
20+
override final def write[J](obj: Seq[Arg.CacheKey], b: Builder[J]): Unit = {
21+
b.beginArray()
22+
builder.withValue(b) {
23+
obj.foreach(stamp)
24+
}
25+
b.endArray()
26+
}
27+
28+
// we actually don't need to read anything back, see https://github.com/sbt/sbt/pull/5513
29+
override final def read[J](
30+
jsOpt: Option[J],
31+
unbuilder: Unbuilder[J]
32+
): Seq[Arg.CacheKey] = ???
33+
}
34+
35+
}

src/main/scala/scalafix/internal/sbt/ScalafixCompletions.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,13 @@ class ScalafixCompletions(
145145
"--syntactic",
146146
"--verbose",
147147
"--version"
148-
).map(literal)
148+
).map(f => literal(f).map(ShellArgs.Extra(_))) ++ Seq(
149+
"--no-cache".^^^(ShellArgs.NoCache)
150+
)
149151
Parser.oneOf(flags) |
150152
hide(string) // catch-all for all args not known to sbt-scalafix
151-
}.map(ShellArgs.Extra(_))
153+
.map(ShellArgs.Extra(_))
154+
}
152155

153156
val rule: ArgP =
154157
fileRule |

src/main/scala/scalafix/internal/sbt/ScalafixInterface.scala

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,45 +17,54 @@ import scala.util.control.NonFatal
1717
sealed trait Arg extends (ScalafixArguments => ScalafixArguments)
1818

1919
object Arg {
20-
case class ToolClasspath(classLoader: URLClassLoader) extends Arg {
20+
sealed trait CacheKey
21+
22+
case class ToolClasspath(classLoader: URLClassLoader)
23+
extends Arg
24+
with CacheKey {
2125
override def apply(sa: ScalafixArguments): ScalafixArguments =
2226
sa.withToolClasspath(classLoader)
2327
}
2428

25-
case class Rules(rules: Seq[String]) extends Arg {
29+
case class Rules(rules: Seq[String]) extends Arg with CacheKey {
2630
override def apply(sa: ScalafixArguments): ScalafixArguments =
2731
sa.withRules(rules.asJava)
2832
}
2933

30-
case class Paths(paths: Seq[Path]) extends Arg {
34+
case class Paths(paths: Seq[Path]) extends Arg { // this one is extracted/stamped directly
3135
override def apply(sa: ScalafixArguments): ScalafixArguments =
3236
sa.withPaths(paths.asJava)
3337
}
3438

35-
case class Config(file: Option[Path]) extends Arg {
39+
case class Config(file: Option[Path]) extends Arg with CacheKey {
3640
override def apply(sa: ScalafixArguments): ScalafixArguments =
3741
sa.withConfig(jutil.Optional.ofNullable(file.orNull))
3842
}
3943

40-
case class ParsedArgs(args: Seq[String]) extends Arg {
44+
case class ParsedArgs(args: Seq[String]) extends Arg with CacheKey {
4145
override def apply(sa: ScalafixArguments): ScalafixArguments =
4246
sa.withParsedArguments(args.asJava)
4347
}
4448

45-
case class ScalaVersion(version: String) extends Arg {
49+
case class ScalaVersion(version: String) extends Arg { //FIXME: with CacheKey {
4650
override def apply(sa: ScalafixArguments): ScalafixArguments =
4751
sa.withScalaVersion(version)
4852
}
4953

50-
case class ScalacOptions(options: Seq[String]) extends Arg {
54+
case class ScalacOptions(options: Seq[String]) extends Arg { //FIXME: with CacheKey {
5155
override def apply(sa: ScalafixArguments): ScalafixArguments =
5256
sa.withScalacOptions(options.asJava)
5357
}
5458

55-
case class Classpath(classpath: Seq[Path]) extends Arg {
59+
case class Classpath(classpath: Seq[Path]) extends Arg { //FIXME: with CacheKey {
5660
override def apply(sa: ScalafixArguments): ScalafixArguments =
5761
sa.withClasspath(classpath.asJava)
5862
}
63+
64+
case object NoCache extends Arg with CacheKey {
65+
override def apply(sa: ScalafixArguments): ScalafixArguments =
66+
sa // caching is currently implemented in sbt-scalafix itself
67+
}
5968
}
6069

6170
class ScalafixInterface private (
@@ -89,7 +98,7 @@ class ScalafixInterface private (
8998
}
9099

91100
def validate(): Option[ScalafixException] =
92-
scalafixArguments.validate().asScala
101+
Option(scalafixArguments.validate().orElse(null))
93102

94103
}
95104

src/main/scala/scalafix/internal/sbt/ShellArgs.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,26 @@ object ShellArgs {
66
case class Rule(value: String) extends Arg
77
case class File(value: String) extends Arg
88
case class Extra(key: String, value: Option[String] = None) extends Arg
9+
case object NoCache extends Arg
910

1011
def apply(args: Seq[Arg]): ShellArgs = {
1112
val rules = List.newBuilder[String]
1213
val files = List.newBuilder[String]
1314
val extra = List.newBuilder[String]
15+
var noCache = false
1416
args.foreach {
1517
case x: Rule => rules += x.value
1618
case x: File => files += x.value
1719
case x: Extra => extra += x.value.foldLeft(x.key)((k, v) => s"$k=$v")
20+
case NoCache => noCache = true
1821
}
19-
ShellArgs(rules.result(), files.result(), extra.result())
22+
ShellArgs(rules.result(), files.result(), extra.result(), noCache)
2023
}
2124
}
2225

2326
case class ShellArgs(
2427
rules: List[String] = Nil,
2528
files: List[String] = Nil,
26-
extra: List[String] = Nil
29+
extra: List[String] = Nil,
30+
noCache: Boolean = false
2731
)

src/main/scala/scalafix/sbt/ScalafixPlugin.scala

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@ import sbt.Keys._
88
import sbt.internal.sbtscalafix.{Compat, JLineAccess}
99
import sbt.plugins.JvmPlugin
1010
import sbt._
11+
import sbt.internal.sbtscalafix.Caching._
1112
import scalafix.interfaces.ScalafixError
1213
import scalafix.internal.sbt._
1314

15+
import scala.util.Try
16+
import scala.util.control.NoStackTrace
17+
1418
object ScalafixPlugin extends AutoPlugin {
1519
override def trigger: PluginTrigger = allRequirements
1620
override def requires: Plugins = JvmPlugin
@@ -24,6 +28,10 @@ object ScalafixPlugin extends AutoPlugin {
2428
"For example: scalafix RemoveUnusedImports. " +
2529
"To run on test sources use test:scalafix."
2630
)
31+
val scalafixCaching: SettingKey[Boolean] =
32+
settingKey[Boolean](
33+
"Cache scalafix invocations (off by default, still experimental)."
34+
)
2735
val scalafixResolvers: SettingKey[Seq[Repository]] =
2836
settingKey[Seq[Repository]](
2937
"Optional list of Maven/Ivy repositories to use for fetching custom rules."
@@ -99,6 +107,7 @@ object ScalafixPlugin extends AutoPlugin {
99107
)
100108
},
101109
scalafixConfig := None, // let scalafix-cli try to infer $CWD/.scalafix.conf
110+
scalafixCaching := false,
102111
scalafixResolvers := ScalafixCoursier.defaultResolvers,
103112
scalafixDependencies := Nil,
104113
commands += ScalafixEnable.command
@@ -158,11 +167,15 @@ object ScalafixPlugin extends AutoPlugin {
158167
scalafixDependencies.in(ThisBuild).value,
159168
scalafixResolvers.in(ThisBuild).value
160169
)
161-
val mainInterface = mainInterface0.withArgs(
162-
Arg.Config(scalafixConf),
163-
Arg.Rules(shell.rules),
164-
Arg.ParsedArgs(shell.extra)
165-
)
170+
val maybeNoCache =
171+
if (shell.noCache || !scalafixCaching.value) Seq(Arg.NoCache) else Nil
172+
val mainInterface = mainInterface0
173+
.withArgs(maybeNoCache: _*)
174+
.withArgs(
175+
Arg.Config(scalafixConf),
176+
Arg.Rules(shell.rules),
177+
Arg.ParsedArgs(shell.extra)
178+
)
166179
val rulesThatWillRun = mainInterface.rulesThatWillRun()
167180
val isSemantic = rulesThatWillRun.exists(_.kind().isSemantic)
168181
if (isSemantic) {
@@ -185,7 +198,7 @@ object ScalafixPlugin extends AutoPlugin {
185198
config: Configuration
186199
): Def.Initialize[Task[Unit]] = Def.task {
187200
val files = filesToFix(shellArgs, config).value
188-
runArgs(mainInterface.withArgs(Arg.Paths(files)), streams.value.log)
201+
runArgs(mainInterface.withArgs(Arg.Paths(files)), streams.value)
189202
}
190203

191204
private def scalafixSemantic(
@@ -209,7 +222,7 @@ object ScalafixPlugin extends AutoPlugin {
209222
Arg.Paths(files),
210223
Arg.Classpath(fullClasspath.value.map(_.data.toPath))
211224
)
212-
runArgs(semanticInterface, streams.value.log)
225+
runArgs(semanticInterface, streams.value)
213226
}
214227
} else {
215228
Def.task {
@@ -227,18 +240,78 @@ object ScalafixPlugin extends AutoPlugin {
227240

228241
private def runArgs(
229242
interface: ScalafixInterface,
230-
logger: Logger
243+
streams: TaskStreams
231244
): Unit = {
232245
val paths = interface.args.collect { case Arg.Paths(paths) => paths }.flatten
233246
if (paths.nonEmpty) {
234247
if (paths.lengthCompare(1) > 0) {
235-
logger.info(s"Running scalafix on ${paths.size} Scala sources")
248+
streams.log.info(s"Running scalafix on ${paths.size} Scala sources")
249+
}
250+
251+
val cacheKeyArgs = interface.args.collect {
252+
case cacheKey: Arg.CacheKey => cacheKey
253+
}
254+
val pathsFilesInfo = FilesInfo
255+
.lastModified(paths.map(_.toFile).toSet)
256+
.asInstanceOf[FilesInfo[ModifiedFileInfo]]
257+
258+
// used to signal that one of the argument cannot be reliably stamped
259+
object StampingImpossible extends RuntimeException with NoStackTrace
260+
261+
implicit val stamper = new CacheKeysStamper {
262+
override protected def stamp: Arg.CacheKey => Unit = {
263+
case Arg.ToolClasspath(classLoader) =>
264+
//FIXME: stamp content of files/directories, don't cache if remote
265+
write(classLoader.getURLs)
266+
case Arg.Rules(rules) =>
267+
//FIXME: don't cache if remote
268+
write(rules)
269+
case Arg.Config(file) =>
270+
//FIXME: stamp default conf file when not provided
271+
write(file.map(p => FileInfo.lastModified(p.toFile)))
272+
case Arg.ParsedArgs(args) =>
273+
val cacheKeys = args.filter {
274+
case "--check" | "--test" =>
275+
// CHECK & IN_PLACE can share the same cache
276+
false
277+
case "--stdout" =>
278+
// --stdout cannot be cached as we don't capture the output to replay it
279+
throw StampingImpossible
280+
case _ => true
281+
}
282+
write(cacheKeys)
283+
case Arg.NoCache =>
284+
throw StampingImpossible
285+
}
286+
}
287+
288+
def diffWithPreviousRun[T](f: Boolean => T): T = {
289+
val tracker = Tracked.inputChanged(streams.cacheDirectory / "inputs") {
290+
(inputChanged: Boolean, _: Seq[Arg.CacheKey]) =>
291+
Tracked.outputChanged(streams.cacheDirectory / "outputs") {
292+
(outputChanged: Boolean, _: FilesInfo[ModifiedFileInfo]) =>
293+
f(inputChanged || outputChanged)
294+
}
295+
}
296+
Try(tracker(cacheKeyArgs)(() => pathsFilesInfo)).recover {
297+
// in sbt 1.x, this is not necessary as any exception thrown during stamping is already silently ignored,
298+
// but having this here helps keeping code as common as possible
299+
// https://github.com/sbt/util/blob/v1.0.0/util-tracking/src/main/scala/sbt/util/Tracked.scala#L180
300+
case _ @StampingImpossible => f(true)
301+
}.get
236302
}
237303

238-
val errors = interface.run()
239-
if (errors.nonEmpty) {
240-
throw new ScalafixFailed(errors.toList)
304+
diffWithPreviousRun { changed =>
305+
if (changed) {
306+
//FIXME: only run on modified/new files, ignore deletions
307+
val errors = interface.run()
308+
if (errors.nonEmpty) throw new ScalafixFailed(errors.toList)
309+
} else {
310+
streams.log.debug(s"already ran on ${paths.length} files")
311+
}
312+
()
241313
}
314+
242315
} else {
243316
() // do nothing
244317
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import _root_.scalafix.sbt.{BuildInfo => Versions}
2+
3+
inThisBuild(
4+
List(
5+
scalaVersion := Versions.scala212,
6+
addCompilerPlugin(scalafixSemanticdb),
7+
scalacOptions += "-Yrangepos",
8+
scalacOptions += "-Ywarn-unused", // for RemoveUnused
9+
scalafixCaching := true
10+
)
11+
)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
rules = [DisableSyntax]
2+
DisableSyntax.noNulls = true
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
rules = [DisableSyntax]
2+
DisableSyntax.noVars = true
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
object Null {
2+
println(null)
3+
}

0 commit comments

Comments
 (0)