diff --git a/build.mill b/build.mill index e92f108a184..70dbe4c21f2 100644 --- a/build.mill +++ b/build.mill @@ -190,9 +190,10 @@ object Deps { val sonatypeCentralClient = ivy"com.lumidion::sonatype-central-client-requests:0.3.0" object RuntimeDeps { - val sbtTestInterface = ivy"com.github.sbt:junit-interface:0.13.2" + val errorProneCore = ivy"com.google.errorprone:error_prone_core:2.31.0" val jupiterInterface = ivy"com.github.sbt.junit:jupiter-interface:0.11.4" - def all = Seq(sbtTestInterface, jupiterInterface) + val sbtTestInterface = ivy"com.github.sbt:junit-interface:0.13.2" + def all = Seq(errorProneCore, jupiterInterface, sbtTestInterface) } /** Used to manage transitive versions. */ @@ -729,6 +730,7 @@ object dist0 extends MillPublishJavaModule { build.contrib.jmh.testDep(), build.contrib.playlib.testDep(), build.contrib.playlib.worker("2.8").testDep(), + build.contrib.errorprone.testDep(), build.bsp.worker.testDep(), build.testkit.testDep() ) diff --git a/contrib/errorprone/readme.adoc b/contrib/errorprone/readme.adoc new file mode 100644 index 00000000000..af67982b5d3 --- /dev/null +++ b/contrib/errorprone/readme.adoc @@ -0,0 +1,31 @@ += Mill ErrorProne Plugin +:page-aliases: Plugin_ErrorProne.adoc + +https://errorprone.info/index[Error Prone] augments the Java compiler's type checker and detect common mistakes at compile time. + +You just need to mix the `ErrorProneModule` into your `JavaModule` and it will automatically run with every compilation. + +.`build.mill.sc`: Enable `ErrorProne` in a module +[source,scala] +---- +package build +import mill._, scalalib._ + +import $ivy.`com.lihaoyi::mill-contrib-errorprone:` +import mill.contrib.errorprone.ErrorProneModule + +object foo extends JavaModule with ErrorProneModule { +} +---- + +== Configuration + +The following configuration options exist: + +`def errorProneVersion: T[String]`:: +The `error-prone` version to use. Defaults to [[BuildInfo.errorProneVersion]], the version used to build and test the module. +Find the latest at https://mvnrepository.com/artifact/com.google.errorprone/error_prone_core[mvnrepository.com] + +`def errorProneOptions: T[Seq[String]]`:: + Options directly given to the `error-prone` processor. +Those are documented as "flags" at https://errorprone.info/docs/flags diff --git a/contrib/errorprone/src/mill/contrib/errorprone/ErrorProneModule.scala b/contrib/errorprone/src/mill/contrib/errorprone/ErrorProneModule.scala new file mode 100644 index 00000000000..ff51b5b4f70 --- /dev/null +++ b/contrib/errorprone/src/mill/contrib/errorprone/ErrorProneModule.scala @@ -0,0 +1,81 @@ +package mill.contrib.errorprone + +import mill.api.PathRef +import mill.{Agg, T} +import mill.scalalib.{Dep, DepSyntax, JavaModule} + +import java.io.File + +/** + * Integrated Error Prone into a [[JavaModule]]. + * + * See https://errorprone.info/index + */ +trait ErrorProneModule extends JavaModule { + + /** The `error-prone` version to use. Defaults to [[BuildInfo.errorProneVersion]]. */ + def errorProneVersion: T[String] = T.input { + BuildInfo.errorProneVersion + } + + /** + * The dependencies of the `error-prone` compiler plugin. + */ + def errorProneDeps: T[Agg[Dep]] = T { + Agg( + ivy"com.google.errorprone:error_prone_core:${errorProneVersion()}" + ) + } + + /** + * The classpath of the `error-prone` compiler plugin. + */ + def errorProneClasspath: T[Agg[PathRef]] = T { + resolveDeps(T.task { errorProneDeps().map(bindDependency()) })() + } + + /** + * Options used to enable and configure the `eror-prone` plugin in the Java compiler. + */ + def errorProneJavacEnableOptions: T[Seq[String]] = T { + val processorPath = errorProneClasspath().map(_.path).mkString(File.pathSeparator) + val enableOpts = Seq( + "-XDcompilePolicy=simple", + "-processorpath", + processorPath, + (Seq("-Xplugin:ErrorProne") ++ errorProneOptions()).mkString(" ") + ) + val java17Options = Option.when(scala.util.Properties.isJavaAtLeast(16))(Seq( + "--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", + "--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", + "--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED" + ).map(o => s"-J${o}")).toSeq.flatten + java17Options ++ enableOpts + } + + /** + * Options directly given to the `error-prone` processor. + * + * Those are documented as "flags" at https://errorprone.info/docs/flags + */ + def errorProneOptions: T[Seq[String]] = T { Seq.empty[String] } + + /** + * Appends the [[errorProneJavacEnableOptions]] to the Java compiler options. + */ + override def javacOptions: T[Seq[String]] = T { + val supOpts = super.javacOptions() + val enableOpts = Option + .when(!supOpts.exists(o => o.startsWith("-Xplugin:ErrorProne")))( + errorProneJavacEnableOptions() + ) + supOpts ++ enableOpts.toSeq.flatten + } +} diff --git a/contrib/errorprone/test/resources/simple/src/ShortSet.java b/contrib/errorprone/test/resources/simple/src/ShortSet.java new file mode 100644 index 00000000000..5b1588d9620 --- /dev/null +++ b/contrib/errorprone/test/resources/simple/src/ShortSet.java @@ -0,0 +1,16 @@ +package simple.src; + +import java.util.HashSet; +import java.util.Set; + +public class ShortSet { + public static void main (String[] args) { + Set s = new HashSet<>(); + for (short i = 0; i < 100; i++) { + s.add(i); + s.remove(i - 1); + } + System.out.println(s.size()); + } +} + diff --git a/contrib/errorprone/test/src/mill/contrib/errorprone/ErrorProneTests.scala b/contrib/errorprone/test/src/mill/contrib/errorprone/ErrorProneTests.scala new file mode 100644 index 00000000000..617697d9a36 --- /dev/null +++ b/contrib/errorprone/test/src/mill/contrib/errorprone/ErrorProneTests.scala @@ -0,0 +1,44 @@ +package mill.contrib.errorprone + +import mill.T +import mill.scalalib.JavaModule +import mill.testkit.{TestBaseModule, UnitTester} +import os.Path +import utest._ + +object ErrorProneTests extends TestSuite { + + object noErrorProne extends TestBaseModule with JavaModule {} + object errorProne extends TestBaseModule with JavaModule with ErrorProneModule {} + object errorProneCustom extends TestBaseModule with JavaModule with ErrorProneModule { + override def errorProneOptions: T[Seq[String]] = T(Seq( + "-XepAllErrorsAsWarnings" + )) + } + + val testModuleSourcesPath: Path = os.Path(sys.env("MILL_TEST_RESOURCE_FOLDER")) / "simple" + + def tests = Tests { + test("reference") { + test("compile") { + val eval = UnitTester(noErrorProne, testModuleSourcesPath) + val res = eval(noErrorProne.compile) + assert(res.isRight) + } + } + test("errorprone") { + test("compileFail") { + val eval = UnitTester(errorProne, testModuleSourcesPath) + val res = eval(errorProne.compile) + assert(res.isLeft) + } + test("compileWarn") { + val eval = UnitTester(errorProneCustom, testModuleSourcesPath, debugEnabled = true) + val Right(opts) = eval(errorProneCustom.javacOptions) + assert(opts.value.exists(_.contains("-XepAllErrorsAsWarnings"))) + val res = eval(errorProneCustom.compile) + assert(res.isRight) + } + } + } +} diff --git a/contrib/package.mill b/contrib/package.mill index f091685d9cb..3c5ee5bda6b 100644 --- a/contrib/package.mill +++ b/contrib/package.mill @@ -17,6 +17,7 @@ import mill.resolve.SelectMode import mill.contrib.buildinfo.BuildInfo import mill.T import mill.define.Cross +import build.Deps // plugins and dependencies import $meta._ @@ -226,4 +227,12 @@ object `package` extends RootModule { def compileModuleDeps = Seq(build.scalalib) def testModuleDeps = super.testModuleDeps ++ Seq(build.scalalib) } + + object errorprone extends ContribModule with BuildInfo { + def compileModuleDeps = Seq(build.scalalib) + def testModuleDeps = super.testModuleDeps ++ Seq(build.scalalib) + def buildInfoPackageName = "mill.contrib.errorprone" + def buildInfoObjectName = "BuildInfo" + def buildInfoMembers = Seq(BuildInfo.Value("errorProneVersion", Deps.RuntimeDeps.errorProneCore.version)) + } } diff --git a/docs/modules/ROOT/pages/Java_Module_Config.adoc b/docs/modules/ROOT/pages/Java_Module_Config.adoc index f738f793134..7639b0cd34e 100644 --- a/docs/modules/ROOT/pages/Java_Module_Config.adoc +++ b/docs/modules/ROOT/pages/Java_Module_Config.adoc @@ -88,3 +88,6 @@ If you are using millw, a more permanent solution could be to set the environmen include::example/javalib/module/13-jni.adoc[] +== Using the ErrorProne plugin to detect code problems + +include::example/javalib/module/14-error-prone.adoc[] diff --git a/example/javalib/module/14-error-prone/build.mill b/example/javalib/module/14-error-prone/build.mill new file mode 100644 index 00000000000..1644a53fb4d --- /dev/null +++ b/example/javalib/module/14-error-prone/build.mill @@ -0,0 +1,31 @@ +// When adding the `ErrorPromeModule` to your `JavaModule`, +// the `error-prone` compiler plugin automatically detects various kind of programming errors. + +package build +import mill._, javalib._ +import mill.contrib.errorprone._ + +import $ivy.`com.lihaoyi::mill-contrib-errorprone:` + +object `package` extends RootModule with JavaModule with ErrorProneModule { + def errorProneOptions = Seq("-XepAllErrorsAsWarnings") +} + +/** See Also: src/example/ShortSet.java */ + +/** Usage + +> ./mill show errorProneOptions +[ + "-XepAllErrorsAsWarnings" +] + +> ./mill compile +[warn] .../src/example/ShortSet.java:11:15: [CollectionIncompatibleType] Argument 'i - 1' should not be passed to this method; its type int is not compatible with its collection's type argument Short +[warn] s.remove(i - 1); +[warn] ^ (see https://errorprone.info/bugpattern/CollectionIncompatibleType) +[warn] 1 warning +[warn] ^ +*/ + + diff --git a/example/javalib/module/14-error-prone/src/example/ShortSet.java b/example/javalib/module/14-error-prone/src/example/ShortSet.java new file mode 100644 index 00000000000..deb61d116ee --- /dev/null +++ b/example/javalib/module/14-error-prone/src/example/ShortSet.java @@ -0,0 +1,16 @@ +package example; + +import java.util.HashSet; +import java.util.Set; + +public class ShortSet { + public static void main (String[] args) { + Set s = new HashSet<>(); + for (short i = 0; i < 100; i++) { + s.add(i); + s.remove(i - 1); + } + System.out.println(s.size()); + } +} + diff --git a/scalalib/worker/src/mill/scalalib/worker/ZincWorkerImpl.scala b/scalalib/worker/src/mill/scalalib/worker/ZincWorkerImpl.scala index cd8e3080e8c..36907d6fbdb 100644 --- a/scalalib/worker/src/mill/scalalib/worker/ZincWorkerImpl.scala +++ b/scalalib/worker/src/mill/scalalib/worker/ZincWorkerImpl.scala @@ -496,6 +496,22 @@ class ZincWorkerImpl( )(implicit ctx: ZincWorkerApi.Ctx): Result[CompilationResult] = { os.makeDir.all(ctx.dest) + val classesDir = + if (compileToJar) ctx.dest / "classes.jar" + else ctx.dest / "classes" + + if (ctx.log.debugEnabled) { + ctx.log.debug( + s"""Compiling: + | javacOptions: ${javacOptions.map("'" + _ + "'").mkString(" ")} + | scalacOptions: ${scalacOptions.map("'" + _ + "'").mkString(" ")} + | sources: ${sources.map("'" + _ + "'").mkString(" ")} + | classpath: ${compileClasspath.map("'" + _ + "'").mkString(" ")} + | output: ${classesDir}""" + .stripMargin + ) + } + reporter.foreach(_.start()) val consoleAppender = ConsoleAppender( @@ -549,10 +565,6 @@ class ZincWorkerImpl( val lookup = MockedLookup(analysisMap) - val classesDir = - if (compileToJar) ctx.dest / "classes.jar" - else ctx.dest / "classes" - val store = fileAnalysisStore(ctx.dest / zincCache) // Fix jdk classes marked as binary dependencies, see https://github.com/com-lihaoyi/mill/pull/1904 diff --git a/testkit/src/mill/testkit/ExampleTester.scala b/testkit/src/mill/testkit/ExampleTester.scala index 20b310a72d3..656d0bce69f 100644 --- a/testkit/src/mill/testkit/ExampleTester.scala +++ b/testkit/src/mill/testkit/ExampleTester.scala @@ -25,7 +25,7 @@ import utest._ * 2. Output lines can be prefixed by `error: ` to indicate we expect that * command to fail. * - * 3. `..` can be used to indicate wildcards, which match anything. These can + * 3. `...` can be used to indicate wildcards, which match anything. These can * be used alone as the entire line, or in the middle of another line * * 4. Every line of stdout/stderr output by the command must match at least