Skip to content

Commit 884832d

Browse files
Merge pull request #102 from bjaglin/caching
Opt-in caching for scalafix invocations
2 parents d101212 + 8b7cb8f commit 884832d

File tree

25 files changed

+804
-124
lines changed

25 files changed

+804
-124
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
matrix:
1414
command:
1515
- "++2.12.11 test scripted"
16-
- "++2.10.7 test 'scripted sbt-scalafix/*'"
16+
- "++2.10.7 test 'scripted sbt-scalafix/* skip-windows/*'"
1717
steps:
1818
- uses: actions/checkout@v2
1919
- uses: olafurpg/setup-scala@v7

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ developers := List(
2727

2828
commands += Command.command("ci-windows") { s =>
2929
"testOnly -- -l SkipWindows" ::
30-
"scripted" ::
30+
"scripted sbt-*/*" ::
3131
s
3232
}
3333

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

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 |
Lines changed: 130 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,111 @@
11
package scalafix.internal.sbt
22

3+
import java.io.PrintStream
34
import java.net.URLClassLoader
5+
import java.nio.file.Path
6+
import java.{util => jutil}
47

58
import com.geirsson.coursiersmall.Repository
69
import sbt._
710
import sbt.internal.sbtscalafix.Compat
8-
import scalafix.interfaces.{ScalafixArguments, Scalafix => ScalafixAPI}
11+
import scalafix.interfaces.{Scalafix => ScalafixAPI, _}
12+
import scalafix.sbt.InvalidArgument
13+
14+
import scala.collection.JavaConverters._
15+
import scala.util.control.NonFatal
16+
17+
sealed trait Arg extends (ScalafixArguments => ScalafixArguments)
18+
19+
object Arg {
20+
21+
sealed trait CacheKey
22+
23+
case class ToolClasspath(classLoader: URLClassLoader)
24+
extends Arg
25+
with CacheKey {
26+
override def apply(sa: ScalafixArguments): ScalafixArguments =
27+
// this effectively overrides any previous URLClassLoader
28+
sa.withToolClasspath(classLoader)
29+
}
30+
31+
case class Rules(rules: Seq[String]) extends Arg with CacheKey {
32+
override def apply(sa: ScalafixArguments): ScalafixArguments =
33+
sa.withRules(rules.asJava)
34+
}
35+
36+
case class Paths(paths: Seq[Path]) extends Arg { // this one is extracted/stamped directly
37+
override def apply(sa: ScalafixArguments): ScalafixArguments =
38+
sa.withPaths(paths.asJava)
39+
}
40+
41+
case class Config(file: Option[Path]) extends Arg with CacheKey {
42+
override def apply(sa: ScalafixArguments): ScalafixArguments =
43+
sa.withConfig(jutil.Optional.ofNullable(file.orNull))
44+
}
45+
46+
case class ParsedArgs(args: Seq[String]) extends Arg with CacheKey {
47+
override def apply(sa: ScalafixArguments): ScalafixArguments =
48+
sa.withParsedArguments(args.asJava)
49+
}
50+
51+
case class ScalaVersion(version: String) extends Arg { //FIXME: with CacheKey {
52+
override def apply(sa: ScalafixArguments): ScalafixArguments =
53+
sa.withScalaVersion(version)
54+
}
55+
56+
case class ScalacOptions(options: Seq[String]) extends Arg { //FIXME: with CacheKey {
57+
override def apply(sa: ScalafixArguments): ScalafixArguments =
58+
sa.withScalacOptions(options.asJava)
59+
}
60+
61+
case class Classpath(classpath: Seq[Path]) extends Arg { //FIXME: with CacheKey {
62+
override def apply(sa: ScalafixArguments): ScalafixArguments =
63+
sa.withClasspath(classpath.asJava)
64+
}
65+
66+
case object NoCache extends Arg with CacheKey {
67+
override def apply(sa: ScalafixArguments): ScalafixArguments =
68+
sa // caching is currently implemented in sbt-scalafix itself
69+
}
70+
}
971

1072
class ScalafixInterface private (
11-
val args: ScalafixArguments,
12-
private val toolClasspath: URLClassLoader
73+
scalafixArguments: ScalafixArguments, // hide it to force usage of withArgs so we can intercept arguments
74+
val args: Seq[Arg]
1375
) {
1476

15-
// Accumulates the classpath via classloader delegation, as args.withToolClasspath() only considers the last call.
77+
private val lastToolClasspath = args.reverse
78+
.collectFirst { case tcp: Arg.ToolClasspath => tcp }
79+
.getOrElse(
80+
throw new IllegalArgumentException(
81+
"a base toolClasspath must be provided"
82+
)
83+
)
84+
.classLoader
85+
86+
private def this(
87+
api: ScalafixAPI,
88+
toolClasspath: URLClassLoader,
89+
mainCallback: ScalafixMainCallback,
90+
printStream: PrintStream
91+
) = this(
92+
api
93+
.newArguments()
94+
.withMainCallback(mainCallback)
95+
.withPrintStream(printStream)
96+
.withToolClasspath(toolClasspath),
97+
Seq(Arg.ToolClasspath(toolClasspath))
98+
)
99+
100+
// Accumulates the classpath via classloader delegation, as only the last Arg.ToolClasspath is considered
16101
//
17102
// We effectively end up with the following class loader hierarchy:
18103
// 1. Meta-project sbt class loader
19104
// - bound to the sbt session
20105
// 2. ScalafixInterfacesClassloader, loading `scalafix-interfaces` from its parent
21106
// - bound to the sbt session
22107
// 3. `scalafix-cli` JARs
108+
// - passed in the constructor
23109
// - bound to the sbt session
24110
// 4. Global, external dependencies
25111
// - present only if custom dependencies were defined
@@ -34,23 +120,40 @@ class ScalafixInterface private (
34120
customResolvers: Seq[Repository],
35121
extraInternalDeps: Seq[File]
36122
): ScalafixInterface = {
37-
if (extraExternalDeps.isEmpty && extraInternalDeps.isEmpty) {
38-
this
39-
} else {
40-
val extraURLs = ScalafixCoursier
41-
.scalafixToolClasspath(
42-
extraExternalDeps,
43-
customResolvers
44-
) ++ extraInternalDeps.map(_.toURI.toURL)
45-
46-
val newToolClasspath =
47-
new URLClassLoader(extraURLs.toArray, toolClasspath)
48-
new ScalafixInterface(
49-
args.withToolClasspath(newToolClasspath),
50-
newToolClasspath
51-
)
123+
val extraURLs = ScalafixCoursier
124+
.scalafixToolClasspath(
125+
extraExternalDeps,
126+
customResolvers
127+
) ++ extraInternalDeps.map(_.toURI.toURL)
128+
129+
if (extraURLs.isEmpty) this
130+
else {
131+
val classpath = new URLClassLoader(extraURLs.toArray, lastToolClasspath)
132+
withArgs(Arg.ToolClasspath(classpath))
52133
}
53134
}
135+
136+
def withArgs(args: Arg*): ScalafixInterface = {
137+
val newScalafixArguments = args.foldLeft(scalafixArguments) { (acc, arg) =>
138+
try arg(acc)
139+
catch { case NonFatal(e) => throw new InvalidArgument(e.getMessage) }
140+
}
141+
new ScalafixInterface(newScalafixArguments, this.args ++ args)
142+
}
143+
144+
def run(): Seq[ScalafixError] =
145+
scalafixArguments.run().toSeq
146+
147+
def availableRules(): Seq[ScalafixRule] =
148+
scalafixArguments.availableRules().asScala
149+
150+
def rulesThatWillRun(): Seq[ScalafixRule] =
151+
try scalafixArguments.rulesThatWillRun().asScala
152+
catch { case NonFatal(e) => throw new InvalidArgument(e.getMessage) }
153+
154+
def validate(): Option[ScalafixException] =
155+
Option(scalafixArguments.validate().orElse(null))
156+
54157
}
55158

56159
object ScalafixInterface {
@@ -61,7 +164,8 @@ object ScalafixInterface {
61164
def fromToolClasspath(
62165
scalafixDependencies: Seq[ModuleID],
63166
scalafixCustomResolvers: Seq[Repository],
64-
logger: Logger = Compat.ConsoleLogger(System.out)
167+
logger: Logger = Compat.ConsoleLogger(System.out),
168+
printStream: PrintStream = System.out
65169
): () => ScalafixInterface =
66170
new LazyValue({ () =>
67171
val jars = ScalafixCoursier.scalafixCliJars(scalafixCustomResolvers)
@@ -71,15 +175,11 @@ object ScalafixInterface {
71175
val classloader = new URLClassLoader(urls, interfacesParent)
72176
val api = ScalafixAPI.classloadInstance(classloader)
73177
val callback = new ScalafixLogger(logger)
74-
75-
val args = api
76-
.newArguments()
77-
.withMainCallback(callback)
78-
79-
new ScalafixInterface(args, classloader).addToolClasspath(
80-
scalafixDependencies,
81-
scalafixCustomResolvers,
82-
Nil
83-
)
178+
new ScalafixInterface(api, classloader, callback, printStream)
179+
.addToolClasspath(
180+
scalafixDependencies,
181+
scalafixCustomResolvers,
182+
Nil
183+
)
84184
})
85185
}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@ package scalafix.internal.sbt
33
import java.nio.file.Path
44

55
import sbt.{CrossVersion, ModuleID}
6-
import scalafix.interfaces.ScalafixArguments
76

87
import scala.collection.mutable.ListBuffer
98

109
class SemanticRuleValidator(ifNotFound: SemanticdbNotFound) {
1110
def findErrors(
1211
files: Seq[Path],
1312
dependencies: Seq[ModuleID],
14-
args: ScalafixArguments
13+
interface: ScalafixInterface
1514
): Seq[String] = {
1615
if (files.isEmpty) Nil
1716
else {
@@ -20,9 +19,10 @@ class SemanticRuleValidator(ifNotFound: SemanticdbNotFound) {
2019
dependencies.exists(_.name.startsWith("semanticdb-scalac"))
2120
if (!hasSemanticdb)
2221
errors += ifNotFound.message
23-
val invalidArguments = args.validate()
24-
if (invalidArguments.isPresent)
25-
errors += invalidArguments.get().getMessage
22+
val invalidArguments = interface.validate()
23+
invalidArguments.foreach { invalidArgument =>
24+
errors += invalidArgument.getMessage
25+
}
2626
errors
2727
}
2828
}

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
)

0 commit comments

Comments
 (0)