diff options
author | Nikolay Tatarinov <5min4eq.unity@gmail.com> | 2018-05-06 11:26:21 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-05-06 11:26:21 +0300 |
commit | f7a02a46f847d4433cd771840fd0b6cc314215d8 (patch) | |
tree | 612a1d01a63a75dedd1c53bbeccf3083b9f52a65 | |
parent | 32c7515d6507e1898562203ea7c9d13a53d933c1 (diff) | |
download | mill-f7a02a46f847d4433cd771840fd0b6cc314215d8.tar.gz mill-f7a02a46f847d4433cd771840fd0b6cc314215d8.tar.bz2 mill-f7a02a46f847d4433cd771840fd0b6cc314215d8.zip |
Scalafmt support (#308)
* add scalafmt module, that formats all sources files on every run
* scalafmt worker that internally chaches reformatted files
* move jvm process call to helper method
* use scala 2.12.4 to resolve scalafmt deps; check for config file existence; add quiet flags to scalafmt CLI
* make a scalafmt worker a singleton
* add tests for scalafmt module
* add reformatAll command
* tests for reformatAll command
* add docs about scalafmt support
7 files changed, 263 insertions, 0 deletions
diff --git a/docs/pages/2 - Configuring Mill.md b/docs/pages/2 - Configuring Mill.md index a229fa72..43191950 100644 --- a/docs/pages/2 - Configuring Mill.md +++ b/docs/pages/2 - Configuring Mill.md @@ -213,6 +213,28 @@ You can use Scala compiler plugins by setting `scalacPluginIvyDeps`. The above example also adds the plugin to `compileIvyDeps`, since that plugin's artifact is needed on the compilation classpath (though not at runtime). +## Reformatting your code + +Mill supports code formatting via [scalafmt](https://scalameta.org/scalafmt/) out of the box. + +To have a formatting per-module you need to make your module extend `mill.scalalib.scalafmt.ScalafmtModule`: + +```scala +// build.sc +import mill._ +import mill.scalalib._ +import mill.scalalib.scalafmt._ + +object foo extends ScalaModule with ScalafmtModule { + def scalaVersion = "2.12.4" +} +``` + +Now you can reformat code with `mill foo.reformat` command. + +You can also reformat your project's code globally with `mill mill.scalalib.scalafmt.ScalafmtModule/reformatAll __.sources` command. +It will reformat all sources that matches `__.sources` query. + ## Common Configuration ```scala diff --git a/scalalib/src/mill/scalalib/scalafmt/ScalafmtModule.scala b/scalalib/src/mill/scalalib/scalafmt/ScalafmtModule.scala new file mode 100644 index 00000000..3b325b4c --- /dev/null +++ b/scalalib/src/mill/scalalib/scalafmt/ScalafmtModule.scala @@ -0,0 +1,58 @@ +package mill.scalalib.scalafmt + +import ammonite.ops.{exists, ls, pwd} +import mill._ +import mill.define._ +import mill.scalalib._ + +trait ScalafmtModule extends JavaModule { + + def reformat(): Command[Unit] = T.command { + ScalafmtWorkerModule + .worker() + .reformat( + filesToFormat(sources()), + scalafmtConfig().head, + scalafmtDeps().map(_.path) + ) + } + + def scalafmtVersion: T[String] = "1.5.1" + + def scalafmtConfig: Sources = T.sources(pwd / ".scalafmt.conf") + + def scalafmtDeps: T[Agg[PathRef]] = T { + Lib.resolveDependencies( + ScalaWorkerModule.repositories, + Lib.depToDependency(_, "2.12.4"), + Seq(ivy"com.geirsson::scalafmt-cli:${scalafmtVersion()}") + ) + } + + protected def filesToFormat(sources: Seq[PathRef]) = { + for { + pathRef <- sources if exists(pathRef.path) + file <- ls.rec(pathRef.path) if file.isFile && file.ext == "scala" + } yield PathRef(file) + } + +} + +object ScalafmtModule extends ExternalModule with ScalafmtModule { + + def reformatAll(sources: mill.main.Tasks[Seq[PathRef]]): Command[Unit] = + T.command { + val files = Task.sequence(sources.value)().flatMap(filesToFormat) + ScalafmtWorkerModule + .worker() + .reformat( + files, + scalafmtConfig().head, + scalafmtDeps().map(_.path) + ) + } + + implicit def millScoptTargetReads[T] = new mill.main.Tasks.Scopt[T]() + + lazy val millDiscover = Discover[this.type] +} diff --git a/scalalib/src/mill/scalalib/scalafmt/ScalafmtWorker.scala b/scalalib/src/mill/scalalib/scalafmt/ScalafmtWorker.scala new file mode 100644 index 00000000..d9921e9d --- /dev/null +++ b/scalalib/src/mill/scalalib/scalafmt/ScalafmtWorker.scala @@ -0,0 +1,58 @@ +package mill.scalalib.scalafmt + +import ammonite.ops.{Path, exists} +import mill._ +import mill.define.{Discover, ExternalModule, Worker} +import mill.modules.Jvm +import mill.util.Ctx + +import scala.collection.mutable + +object ScalafmtWorkerModule extends ExternalModule { + def worker: Worker[ScalafmtWorker] = T.worker { new ScalafmtWorker() } + + lazy val millDiscover = Discover[this.type] +} + +private[scalafmt] class ScalafmtWorker { + private val reformatted: mutable.Map[Path, Int] = mutable.Map.empty + private var configSig: Int = 0 + + def reformat(input: Seq[PathRef], + scalafmtConfig: PathRef, + scalafmtClasspath: Agg[Path])(implicit ctx: Ctx): Unit = { + val toFormat = + if (scalafmtConfig.sig != configSig) input + else + input.filterNot(ref => reformatted.get(ref.path).contains(ref.sig)) + + if (toFormat.nonEmpty) { + ctx.log.info(s"Formatting ${toFormat.size} Scala sources") + reformatAction(toFormat.map(_.path), + scalafmtConfig.path, + scalafmtClasspath) + reformatted ++= toFormat.map { ref => + val updRef = PathRef(ref.path) + updRef.path -> updRef.sig + } + configSig = scalafmtConfig.sig + } else { + ctx.log.info(s"Everything is formatted already") + } + } + + private val cliFlags = Seq("--non-interactive", "--quiet") + + private def reformatAction(toFormat: Seq[Path], + config: Path, + classpath: Agg[Path])(implicit ctx: Ctx) = { + val configFlags = + if (exists(config)) Seq("--config", config.toString) else Seq.empty + Jvm.subprocess( + "org.scalafmt.cli.Cli", + classpath, + mainArgs = toFormat.map(_.toString) ++ configFlags ++ cliFlags + ) + } + +} diff --git a/scalalib/test/resources/scalafmt/core/resources/application.conf b/scalalib/test/resources/scalafmt/core/resources/application.conf new file mode 100644 index 00000000..f5f89257 --- /dev/null +++ b/scalalib/test/resources/scalafmt/core/resources/application.conf @@ -0,0 +1 @@ +foo.bar = 2 diff --git a/scalalib/test/resources/scalafmt/core/src/Main.scala b/scalalib/test/resources/scalafmt/core/src/Main.scala new file mode 100644 index 00000000..a5c74235 --- /dev/null +++ b/scalalib/test/resources/scalafmt/core/src/Main.scala @@ -0,0 +1,7 @@ + +object Main extends App{ + val person = Person.fromString("rockjam:25") + val greeting = s"hello ${person.name}, your age is: ${person.age}" + println(greeting) +} + diff --git a/scalalib/test/resources/scalafmt/core/src/Person.scala b/scalalib/test/resources/scalafmt/core/src/Person.scala new file mode 100644 index 00000000..b296cf85 --- /dev/null +++ b/scalalib/test/resources/scalafmt/core/src/Person.scala @@ -0,0 +1,12 @@ +object Person { + def fromString(s: String): Person = { + val Array(name, age) = s.split(":") + Person( + name, + age.toInt) + } +} + + + +case class Person(name: String, age: Int) diff --git a/scalalib/test/src/mill/scalalib/scalafmt/ScalafmtTests.scala b/scalalib/test/src/mill/scalalib/scalafmt/ScalafmtTests.scala new file mode 100644 index 00000000..fcff52a3 --- /dev/null +++ b/scalalib/test/src/mill/scalalib/scalafmt/ScalafmtTests.scala @@ -0,0 +1,105 @@ +package mill.scalalib.scalafmt + +import ammonite.ops._ +import mill.main.Tasks +import mill.scalalib.ScalaModule +import mill.util.{TestEvaluator, TestUtil} +import utest._ +import utest.framework.TestPath + +object ScalafmtTests extends TestSuite { + + trait TestBase extends TestUtil.BaseModule { + def millSourcePath = + TestUtil.getSrcPathBase() / millOuterCtx.enclosing.split('.') + } + + object ScalafmtTestModule extends TestBase { + object core extends ScalaModule with ScalafmtModule { + def scalaVersion = "2.12.4" + } + } + + val resourcePath = pwd / 'scalalib / 'test / 'resources / 'scalafmt + + def workspaceTest[T, M <: TestUtil.BaseModule]( + m: M, + resourcePath: Path = resourcePath)(t: TestEvaluator[M] => T)( + implicit tp: TestPath): T = { + val eval = new TestEvaluator(m) + rm(m.millSourcePath) + rm(eval.outPath) + mkdir(m.millSourcePath / up) + cp(resourcePath, m.millSourcePath) + t(eval) + } + + def tests: Tests = Tests { + 'scalafmt - { + def checkReformat(reformatCommand: mill.define.Command[Unit]) = + workspaceTest(ScalafmtTestModule) { eval => + val before = getProjectFiles(ScalafmtTestModule.core, eval) + + // first reformat + val Right(_) = eval.apply(reformatCommand) + + val firstReformat = getProjectFiles(ScalafmtTestModule.core, eval) + + assert( + firstReformat("Main.scala").modifyTime > before("Main.scala").modifyTime, + firstReformat("Main.scala").content != before("Main.scala").content, + firstReformat("Person.scala").modifyTime > before("Person.scala").modifyTime, + firstReformat("Person.scala").content != before("Person.scala").content, + // resources files aren't modified + firstReformat("application.conf").modifyTime == before( + "application.conf").modifyTime + ) + + // cached reformat + val Right(_) = eval.apply(reformatCommand) + + val cached = getProjectFiles(ScalafmtTestModule.core, eval) + + assert( + cached("Main.scala").modifyTime == firstReformat("Main.scala").modifyTime, + cached("Person.scala").modifyTime == firstReformat("Person.scala").modifyTime, + cached("application.conf").modifyTime == firstReformat( + "application.conf").modifyTime + ) + + // reformat after change + write.over(cached("Main.scala").path, + cached("Main.scala").content + "\n object Foo") + + val Right(_) = eval.apply(reformatCommand) + + val afterChange = getProjectFiles(ScalafmtTestModule.core, eval) + + assert( + afterChange("Main.scala").modifyTime > cached("Main.scala").modifyTime, + afterChange("Person.scala").modifyTime == cached("Person.scala").modifyTime, + afterChange("application.conf").modifyTime == cached( + "application.conf").modifyTime + ) + } + + 'reformat - checkReformat(ScalafmtTestModule.core.reformat()) + 'reformatAll - checkReformat( + ScalafmtModule.reformatAll(Tasks(Seq(ScalafmtTestModule.core.sources)))) + } + } + + case class FileInfo(content: String, modifyTime: Long, path: Path) + + def getProjectFiles(m: ScalaModule, eval: TestEvaluator[_]) = { + val Right((sources, _)) = eval.apply(m.sources) + val Right((resources, _)) = eval.apply(m.resources) + + val sourcesFiles = sources.flatMap(p => ls.rec(p.path)) + val resourcesFiles = resources.flatMap(p => ls.rec(p.path)) + (sourcesFiles ++ resourcesFiles).map { p => + p.name -> FileInfo(read(p), p.mtime.toMillis, p) + }.toMap + } + +} |