Skip to content

Commit

Permalink
Add ErrorProne plugin (#3460)
Browse files Browse the repository at this point in the history
This adds a new `ErrorProne` plugin to the `contrib` section of Mill.

It provides the `ErrorProneModule` `trait`, which can be mixed into
`JavaModule`s. Derived modules like `ScalaModule` are also supported, as
long as they support the `javacOptions` task to configure the Java
compiler.

We use the [Error Prone](https://errorprone.info/index) project as a
compiler plugin. Although this is a simple javac compiler plugin, it's
usage isn't trivial. Fetching the correct classpath and supplying
options can be tricky, since they need to be provided in a special form.
Also using with JVMs starting from version 16 requires special options
handling due to the new more restrictive module classpath. This plugin
is handling all of the known issues and make its use easy for the user.

Additional configuration options are supported via the
`errorProneOptions` target.

Here is a usage example:

```scala
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")
}
```

Fix #3447

Pull request: #3460
---------

Co-authored-by: Li Haoyi <haoyi.sg@gmail.com>
  • Loading branch information
lefou and lihaoyi authored Sep 8, 2024
1 parent 7ba98c8 commit 221a23e
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 7 deletions.
6 changes: 4 additions & 2 deletions build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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()
)
Expand Down
31 changes: 31 additions & 0 deletions contrib/errorprone/readme.adoc
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
}
}
16 changes: 16 additions & 0 deletions contrib/errorprone/test/resources/simple/src/ShortSet.java
Original file line number Diff line number Diff line change
@@ -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<Short> s = new HashSet<>();
for (short i = 0; i < 100; i++) {
s.add(i);
s.remove(i - 1);
}
System.out.println(s.size());
}
}

Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
9 changes: 9 additions & 0 deletions contrib/package.mill
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -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))
}
}
3 changes: 3 additions & 0 deletions docs/modules/ROOT/pages/Java_Module_Config.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
31 changes: 31 additions & 0 deletions example/javalib/module/14-error-prone/build.mill
Original file line number Diff line number Diff line change
@@ -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] ^
*/


16 changes: 16 additions & 0 deletions example/javalib/module/14-error-prone/src/example/ShortSet.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package example;

import java.util.HashSet;
import java.util.Set;

public class ShortSet {
public static void main (String[] args) {
Set<Short> s = new HashSet<>();
for (short i = 0; i < 100; i++) {
s.add(i);
s.remove(i - 1);
}
System.out.println(s.size());
}
}

20 changes: 16 additions & 4 deletions scalalib/worker/src/mill/scalalib/worker/ZincWorkerImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion testkit/src/mill/testkit/ExampleTester.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 221a23e

Please sign in to comment.