diff --git a/contrib/checkstyle/readme.adoc b/contrib/checkstyle/readme.adoc new file mode 100644 index 00000000000..24c2f7743b6 --- /dev/null +++ b/contrib/checkstyle/readme.adoc @@ -0,0 +1,128 @@ += Checkstyle +:page-aliases: Plugin_Checkstyle.adoc + +Performs quality checks on Java source files using https://checkstyle.org[Checkstyle] and generates reports from these checks. + +== CheckstyleModule + +To use this plugin in a Java/Scala module, + +1. Extend `mill.contrib.checkstyle.CheckstyleModule`. +2. Define a https://checkstyle.org/config.html[configuration] file `checkstyle-config.xml`. +3. Run the `checkstyle` command. + +=== checkstyle + +- flags +[source,sh] +---- + +// if an exception should be raised when violations are found +checkstyle --check + +// if Checkstyle output report should be written to System.out +checkstyle --stdout +---- + +- sources (optional) +[source,sh] +---- +// incorrect paths will cause a command failure +// checkstyle a/b + +// you can specify paths relative to millSourcePath +checkstyle src/a/b + +// process a single file +checkstyle src/a/B.java + +// process multiple sources +checkstyle src/a/b src/c/d src/e/F.java + +// process with flags +checkstyle --check --stdout src/a/b src/c/d + +// process all module sources +checkstyle +---- + +=== Shared configuration + +To share `checkstyle-config.xml` across modules, adapt the following example. +[source,scala] +---- +import mill._ +import mill.contrib.checkstyle.CheckstyleModule +import mill.scalalib._ + +object foo extends Module { + + object bar extends MyModule + object baz extends Module { + object fizz extends MyModule + object buzz extends MyModule + } + + trait MyModule extends JavaModule with CheckstyleModule { + + override def checkstyleConfig = T { + api.PathRef(T.workspace / "checkstyle-config.xml") + } + } +} +---- + + +=== Limitations + +- Version `6.3` or above is required for `plain` and `xml` formats. +- Setting `checkstyleOptions` might cause failures with legacy versions. + +== CheckstyleXsltModule + +This plugin extends the `mill.contrib.checkstyle.CheckstyleModule` with the ability to generate reports by applying https://www.w3.org/TR/xslt/[XSL Transformations] on a Checkstyle output report. + +=== Auto detect XSL Transformations + +XSLT files are detected automatically provided a prescribed directory structure is followed. +[source,scala] +---- +/** + * checkstyle-xslt + * ├─ html + * │ ├─ xslt0.xml + * │ └─ xslt1.xml + * └─ pdf + * ├─ xslt1.xml + * └─ xslt2.xml + * + * html/xslt0.xml -> xslt0.html + * html/xslt1.xml -> xslt1.html + * pdf/xslt1.xml -> xslt1.pdf + * pdf/xslt2.xml -> xslt2.pdf + */ +---- + +=== Specify XSL Transformations manually + +For a custom setup, adapt the following example. +[source,scala] +---- +import mill._ +import mill.api.PathRef +import mill.contrib.checkstyle.CheckstyleXsltModule +import mill.contrib.checkstyle.CheckstyleXsltReport +import mill.scalalib._ + +object foo extends JavaModule with CheckstyleXsltModule { + + override def checkstyleXsltReports = T { + Set( + CheckstyleXsltReport( + PathRef(millSourcePath / "checkstyle-no-frames.xml"), + PathRef(T.dest / "checkstyle-no-frames.html"), + ) + ) + } +} +---- diff --git a/contrib/checkstyle/src/mill/contrib/checkstyle/CheckstyleArgs.scala b/contrib/checkstyle/src/mill/contrib/checkstyle/CheckstyleArgs.scala new file mode 100644 index 00000000000..b075e2ea2b1 --- /dev/null +++ b/contrib/checkstyle/src/mill/contrib/checkstyle/CheckstyleArgs.scala @@ -0,0 +1,21 @@ +package mill.contrib.checkstyle + +import mainargs.{Leftover, ParserForClass, main} + +/** + * Arguments for [[CheckstyleModule.checkstyle]]. + * + * @param check if an exception should be raised when violations are found + * @param stdout if Checkstyle should output report to [[System.out]] + * @param sources (optional) files(s) or folder(s) to process + */ +@main(doc = "arguments for CheckstyleModule.checkstyle") +case class CheckstyleArgs( + check: Boolean = false, + stdout: Boolean = false, + sources: Leftover[String] +) +object CheckstyleArgs { + + implicit val PFC: ParserForClass[CheckstyleArgs] = ParserForClass[CheckstyleArgs] +} diff --git a/contrib/checkstyle/src/mill/contrib/checkstyle/CheckstyleModule.scala b/contrib/checkstyle/src/mill/contrib/checkstyle/CheckstyleModule.scala new file mode 100644 index 00000000000..203c67978cd --- /dev/null +++ b/contrib/checkstyle/src/mill/contrib/checkstyle/CheckstyleModule.scala @@ -0,0 +1,106 @@ +package mill +package contrib.checkstyle + +import mill.api.{Loose, PathRef} +import mill.scalalib.{DepSyntax, JavaModule} +import mill.util.Jvm + +/** + * Performs quality checks on Java source files using [[https://checkstyle.org/ Checkstyle]]. + */ +trait CheckstyleModule extends JavaModule { + + /** + * Runs [[https://checkstyle.org/cmdline.html#Command_line_usage Checkstyle]] and returns one of + * - number of violations found + * - program exit code + * + * @note [[sources]] are processed when no [[CheckstyleArgs.sources]] are specified. + */ + def checkstyle(@mainargs.arg checkstyleArgs: CheckstyleArgs): Command[Int] = T.command { + + val CheckstyleArgs(check, stdout, leftover) = checkstyleArgs + + val output = checkstyleOutput().path + val args = checkstyleOptions() ++ + Seq("-c", checkstyleConfig().path.toString()) ++ + Seq("-f", checkstyleFormat()) ++ + (if (stdout) Seq.empty else Seq("-o", output.toString())) ++ + (if (leftover.value.nonEmpty) leftover.value else sources().map(_.path.toString())) + + T.log.info("running checkstyle ...") + T.log.debug(s"with $args") + + val exitCode = Jvm.callSubprocess( + mainClass = "com.puppycrawl.tools.checkstyle.Main", + classPath = checkstyleClasspath().map(_.path), + mainArgs = args, + workingDir = millSourcePath, // allow passing relative paths for sources like src/a/b + check = false + ).exitCode + + val reported = os.exists(output) + if (reported) { + T.log.info(s"checkstyle output report at $output") + } + + if (exitCode == 0) { + T.log.info("checkstyle found no violation") + } else if (exitCode < 0 || !(reported || stdout)) { + T.log.error( + s"checkstyle exit($exitCode); please check command arguments, plugin settings or try with another version" + ) + throw new UnsupportedOperationException(s"checkstyle exit($exitCode)") + } else if (check) { + throw new RuntimeException(s"checkstyle found $exitCode violation(s)") + } else { + T.log.error(s"checkstyle found $exitCode violation(s)") + } + + exitCode + } + + /** + * Classpath for running Checkstyle. + */ + def checkstyleClasspath: T[Loose.Agg[PathRef]] = T { + defaultResolver().resolveDeps( + Agg(ivy"com.puppycrawl.tools:checkstyle:${checkstyleVersion()}") + ) + } + + /** + * Checkstyle configuration file. Defaults to `checkstyle-config.xml`. + */ + def checkstyleConfig: T[PathRef] = T { + PathRef(millSourcePath / "checkstyle-config.xml") + } + + /** + * Checkstyle output format (` plain | sarif | xml `). Defaults to `plain`. + */ + def checkstyleFormat: T[String] = T { + "plain" + } + + /** + * Additional arguments for Checkstyle. + */ + def checkstyleOptions: T[Seq[String]] = T { + Seq.empty[String] + } + + /** + * Checkstyle output report. + */ + def checkstyleOutput: T[PathRef] = T { + PathRef(T.dest / s"checkstyle-output.${checkstyleFormat()}") + } + + /** + * Checkstyle version. + */ + def checkstyleVersion: T[String] = T { + "10.18.1" + } +} diff --git a/contrib/checkstyle/src/mill/contrib/checkstyle/CheckstyleXsltModule.scala b/contrib/checkstyle/src/mill/contrib/checkstyle/CheckstyleXsltModule.scala new file mode 100644 index 00000000000..a9de7d2fe34 --- /dev/null +++ b/contrib/checkstyle/src/mill/contrib/checkstyle/CheckstyleXsltModule.scala @@ -0,0 +1,98 @@ +package mill +package contrib.checkstyle + +import javax.xml.transform.TransformerFactory +import javax.xml.transform.stream.{StreamResult, StreamSource} + +/** + * Extends [[CheckstyleModule]] with the ability to generate [[CheckstyleXsltReport]]s. + */ +trait CheckstyleXsltModule extends CheckstyleModule { + + /** + * Runs [[CheckstyleModule.checkstyle]] and uses [[CheckstyleModule.checkstyleOutput]] to generate [[checkstyleXsltReports]]. + */ + override def checkstyle(@mainargs.arg checkstyleArgs: CheckstyleArgs): Command[Int] = T.command { + val numViolations = super.checkstyle(checkstyleArgs)() + val checkOutput = checkstyleOutput().path + + if (os.exists(checkOutput)) { + checkstyleXsltReports().foreach { + case CheckstyleXsltReport(xslt, output) => + val xsltSource = new StreamSource(xslt.path.getInputStream) + xsltSource.setSystemId(xslt.path.toIO) // so that relative URI references can be resolved + + val checkSource = + new StreamSource(checkOutput.getInputStream) + + val outputResult = + new StreamResult(os.write.outputStream(output.path, createFolders = true)) + + T.log.info(s"transforming checkstyle output report with $xslt") + + TransformerFactory.newInstance() + .newTransformer(xsltSource) + .transform(checkSource, outputResult) + + T.log.info(s"transformed output report at $output") + } + } + + numViolations + } + + /** + * `xml` + */ + final override def checkstyleFormat: T[String] = T { + "xml" + } + + /** + * Set of [[CheckstyleXsltReport]]s. + * + * The default implementation maps XSLT files, under `checkstyle-xslt`, as depicted below: + * {{{ + * + * checkstyle-xslt + * ├─ html + * │ ├─ xslt0.xml + * │ └─ xslt1.xml + * └─ pdf + * ├─ xslt1.xml + * └─ xslt2.xml + * + * html/xslt0.xml -> xslt0.html + * html/xslt1.xml -> xslt1.html + * pdf/xslt1.xml -> xslt1.pdf + * pdf/xslt2.xml -> xslt2.pdf + * + * }}} + */ + def checkstyleXsltReports: T[Set[CheckstyleXsltReport]] = T { + val dir = millSourcePath / "checkstyle-xslt" + + if (os.exists(dir)) { + val dest = T.dest + os.list(dir) + .iterator + .filter(os.isDir) + .flatMap(childDir => + os.list(childDir) + .iterator + .filter(os.isFile) + .filter(_.ext == "xml") + .map(xslt => + CheckstyleXsltReport( + PathRef(xslt), + PathRef(dest / s"${xslt.baseName}.${childDir.last}") + ) + ) + ) + .toSet + } else { + T.log.info(s"expected XSLT files under $dir") + Set.empty[CheckstyleXsltReport] + } + } +} diff --git a/contrib/checkstyle/src/mill/contrib/checkstyle/CheckstyleXsltReport.scala b/contrib/checkstyle/src/mill/contrib/checkstyle/CheckstyleXsltReport.scala new file mode 100644 index 00000000000..2235bf30b77 --- /dev/null +++ b/contrib/checkstyle/src/mill/contrib/checkstyle/CheckstyleXsltReport.scala @@ -0,0 +1,17 @@ +package mill.contrib.checkstyle + +import mill.api.PathRef + +/** + * A report obtained by transforming a Checkstyle output report. + * + * @param xslt path to an [[https://www.w3.org/TR/xslt/ XSLT]] file + * @param output path to the transformed output report + */ +case class CheckstyleXsltReport(xslt: PathRef, output: PathRef) +object CheckstyleXsltReport { + + import upickle.default._ + + implicit val RW: ReadWriter[CheckstyleXsltReport] = macroRW[CheckstyleXsltReport] +} diff --git a/contrib/checkstyle/test/resources/compatible-java/checkstyle-config.xml b/contrib/checkstyle/test/resources/compatible-java/checkstyle-config.xml new file mode 100644 index 00000000000..f0298e17b39 --- /dev/null +++ b/contrib/checkstyle/test/resources/compatible-java/checkstyle-config.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/contrib/checkstyle/test/resources/compatible-java/src/com/etsy/sbt/TestClass.java b/contrib/checkstyle/test/resources/compatible-java/src/com/etsy/sbt/TestClass.java new file mode 100644 index 00000000000..b76bfb5fe4a --- /dev/null +++ b/contrib/checkstyle/test/resources/compatible-java/src/com/etsy/sbt/TestClass.java @@ -0,0 +1,11 @@ +package com.etsy.sbt; + +public class TestClass { + + private TestClass() { + } + + public static void main(String[] args) { + System.out.println("Hello, world!"); + } +} diff --git a/contrib/checkstyle/test/resources/compatible-scala/checkstyle-config.xml b/contrib/checkstyle/test/resources/compatible-scala/checkstyle-config.xml new file mode 100644 index 00000000000..f0298e17b39 --- /dev/null +++ b/contrib/checkstyle/test/resources/compatible-scala/checkstyle-config.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/contrib/checkstyle/test/resources/compatible-scala/checkstyle-xslt/html/noframes.xml b/contrib/checkstyle/test/resources/compatible-scala/checkstyle-xslt/html/noframes.xml new file mode 100644 index 00000000000..b9c5e7106d1 --- /dev/null +++ b/contrib/checkstyle/test/resources/compatible-scala/checkstyle-xslt/html/noframes.xml @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +

CheckStyle Audit

+
Designed for use with CheckStyle.
+
+ + + +
+ + + +
+ + + + + +

+

+ +


+ + + + +
+ + + +

Files

+ + + + + + + + + + + + + + +
NameErrors
+ + + + + +
+
+ + + + +

File

+ + + + + + + + + + + + + +
Error DescriptionLine
+ + + +
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + +
FilesErrors
+ + + +
+
+ + + + a + b + + +
\ No newline at end of file diff --git a/contrib/checkstyle/test/resources/compatible-scala/src/com/etsy/sbt/TestClass.java b/contrib/checkstyle/test/resources/compatible-scala/src/com/etsy/sbt/TestClass.java new file mode 100644 index 00000000000..b76bfb5fe4a --- /dev/null +++ b/contrib/checkstyle/test/resources/compatible-scala/src/com/etsy/sbt/TestClass.java @@ -0,0 +1,11 @@ +package com.etsy.sbt; + +public class TestClass { + + private TestClass() { + } + + public static void main(String[] args) { + System.out.println("Hello, world!"); + } +} diff --git a/contrib/checkstyle/test/resources/compatible-scala/src/com/etsy/sbt/TestClass.scala b/contrib/checkstyle/test/resources/compatible-scala/src/com/etsy/sbt/TestClass.scala new file mode 100644 index 00000000000..93cecc34fd5 --- /dev/null +++ b/contrib/checkstyle/test/resources/compatible-scala/src/com/etsy/sbt/TestClass.scala @@ -0,0 +1,7 @@ +package com.etsy.sbt + +object TestClass { + +// Checkstyle should ignore a Scala file +// introduce a compile error to trigger failure in case this file is processed +//} diff --git a/contrib/checkstyle/test/resources/non-compatible/checkstyle-config.xml b/contrib/checkstyle/test/resources/non-compatible/checkstyle-config.xml new file mode 100644 index 00000000000..4bd20f0438c --- /dev/null +++ b/contrib/checkstyle/test/resources/non-compatible/checkstyle-config.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/contrib/checkstyle/test/resources/non-compatible/checkstyle-xslt/htm/noframes-copy.xml b/contrib/checkstyle/test/resources/non-compatible/checkstyle-xslt/htm/noframes-copy.xml new file mode 100644 index 00000000000..b9c5e7106d1 --- /dev/null +++ b/contrib/checkstyle/test/resources/non-compatible/checkstyle-xslt/htm/noframes-copy.xml @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +

CheckStyle Audit

+
Designed for use with CheckStyle.
+
+ + + +
+ + + +
+ + + + + +

+

+ +


+ + + + +
+ + + +

Files

+ + + + + + + + + + + + + + +
NameErrors
+ + + + + +
+
+ + + + +

File

+ + + + + + + + + + + + + +
Error DescriptionLine
+ + + +
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + +
FilesErrors
+ + + +
+
+ + + + a + b + + +
\ No newline at end of file diff --git a/contrib/checkstyle/test/resources/non-compatible/checkstyle-xslt/htm/noframes.xml b/contrib/checkstyle/test/resources/non-compatible/checkstyle-xslt/htm/noframes.xml new file mode 100644 index 00000000000..b9c5e7106d1 --- /dev/null +++ b/contrib/checkstyle/test/resources/non-compatible/checkstyle-xslt/htm/noframes.xml @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +

CheckStyle Audit

+
Designed for use with CheckStyle.
+
+ + + +
+ + + +
+ + + + + +

+

+ +


+ + + + +
+ + + +

Files

+ + + + + + + + + + + + + + +
NameErrors
+ + + + + +
+
+ + + + +

File

+ + + + + + + + + + + + + +
Error DescriptionLine
+ + + +
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + +
FilesErrors
+ + + +
+
+ + + + a + b + + +
\ No newline at end of file diff --git a/contrib/checkstyle/test/resources/non-compatible/checkstyle-xslt/htm/readme.txt b/contrib/checkstyle/test/resources/non-compatible/checkstyle-xslt/htm/readme.txt new file mode 100644 index 00000000000..adc8c3d80c2 --- /dev/null +++ b/contrib/checkstyle/test/resources/non-compatible/checkstyle-xslt/htm/readme.txt @@ -0,0 +1 @@ +This file should be skipped by the scan in CheckstyleXsltModule. \ No newline at end of file diff --git a/contrib/checkstyle/test/resources/non-compatible/checkstyle-xslt/html/noframes-copy.xml b/contrib/checkstyle/test/resources/non-compatible/checkstyle-xslt/html/noframes-copy.xml new file mode 100644 index 00000000000..b9c5e7106d1 --- /dev/null +++ b/contrib/checkstyle/test/resources/non-compatible/checkstyle-xslt/html/noframes-copy.xml @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +

CheckStyle Audit

+
Designed for use with CheckStyle.
+
+ + + +
+ + + +
+ + + + + +

+

+ +


+ + + + +
+ + + +

Files

+ + + + + + + + + + + + + + +
NameErrors
+ + + + + +
+
+ + + + +

File

+ + + + + + + + + + + + + +
Error DescriptionLine
+ + + +
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + +
FilesErrors
+ + + +
+
+ + + + a + b + + +
\ No newline at end of file diff --git a/contrib/checkstyle/test/resources/non-compatible/checkstyle-xslt/html/noframes.xml b/contrib/checkstyle/test/resources/non-compatible/checkstyle-xslt/html/noframes.xml new file mode 100644 index 00000000000..b9c5e7106d1 --- /dev/null +++ b/contrib/checkstyle/test/resources/non-compatible/checkstyle-xslt/html/noframes.xml @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +

CheckStyle Audit

+
Designed for use with CheckStyle.
+
+ + + +
+ + + +
+ + + + + +

+

+ +


+ + + + +
+ + + +

Files

+ + + + + + + + + + + + + + +
NameErrors
+ + + + + +
+
+ + + + +

File

+ + + + + + + + + + + + + +
Error DescriptionLine
+ + + +
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + +
FilesErrors
+ + + +
+
+ + + + a + b + + +
\ No newline at end of file diff --git a/contrib/checkstyle/test/resources/non-compatible/src/blocks/ArrayTrailingComma.java b/contrib/checkstyle/test/resources/non-compatible/src/blocks/ArrayTrailingComma.java new file mode 100644 index 00000000000..eef26be6246 --- /dev/null +++ b/contrib/checkstyle/test/resources/non-compatible/src/blocks/ArrayTrailingComma.java @@ -0,0 +1,39 @@ +package blocks; + +// https://checkstyle.org/checks/blocks/avoidnestedblocks.html#ArrayTrailingComma +public class ArrayTrailingComma { + + public class Example1 { + int[] numbers = {1, 2, 3}; + boolean[] bools = { + true, + true, + false // violation + }; + + String[][] text = {{},{},}; + + double[][] decimals = { + {0.5, 2.3, 1.1,}, + {1.7, 1.9, 0.6}, + {0.8, 7.4, 6.5} // violation + }; + + char[] chars = {'a', 'b', 'c' + }; + + String[] letters = { + "a", "b", "c"}; + + int[] a1 = new int[]{ + 1, + 2 + , + }; + + int[] a2 = new int[]{ + 1, + 2 + ,}; + } +} diff --git a/contrib/checkstyle/test/resources/non-compatible/src/coding/EmptyStatement.java b/contrib/checkstyle/test/resources/non-compatible/src/coding/EmptyStatement.java new file mode 100644 index 00000000000..6937cd0bcaa --- /dev/null +++ b/contrib/checkstyle/test/resources/non-compatible/src/coding/EmptyStatement.java @@ -0,0 +1,17 @@ +package coding; + +// https://checkstyle.org/checks/coding/emptystatement.html#EmptyStatement +public class EmptyStatement { + + public class Example1 { + public void foo() { + int i = 5; + if(i > 3); // violation + i++; + for (i = 0; i < 5; i++); // violation + i++; + while (i > 10) + i++; + } + } +} diff --git a/contrib/checkstyle/test/src/mill/contrib/checkstyle/CheckstyleModuleTest.scala b/contrib/checkstyle/test/src/mill/contrib/checkstyle/CheckstyleModuleTest.scala new file mode 100644 index 00000000000..162e1562f57 --- /dev/null +++ b/contrib/checkstyle/test/src/mill/contrib/checkstyle/CheckstyleModuleTest.scala @@ -0,0 +1,208 @@ +package mill +package contrib.checkstyle + +import mainargs.Leftover +import mill.scalalib.{JavaModule, ScalaModule} +import mill.testkit.{TestBaseModule, UnitTester} +import utest._ + +object CheckstyleModuleTest extends TestSuite { + + def tests: Tests = Tests { + + val resources: os.Path = os.Path(sys.env("MILL_TEST_RESOURCE_FOLDER")) + + // violations (for version 10.18.1) in "non-compatible" module + val violations: Seq[String] = + Seq.fill(2)("Array should contain trailing comma") ++ + Seq.fill(2)("Empty statement") + + test("arguments") { + + test("check") { + + intercept[RuntimeException]( + testJava(resources / "non-compatible", check = true) + ) + } + + test("stdout") { + + assert( + testJava(resources / "non-compatible", violations = violations, stdout = true) + ) + } + + test("sources") { + + assert( + testJava( + resources / "non-compatible", + violations = violations.take(2), + sources = Seq("src/blocks") + ), + testJava( + resources / "non-compatible", + violations = violations.take(2) ++ violations.take(2), + sources = Seq("src/blocks", "src/blocks/ArrayTrailingComma.java") + ), + testJava( + resources / "non-compatible", + violations = violations, + sources = Seq("src/blocks", "src/coding") + ) + ) + + intercept[UnsupportedOperationException]( + testJava(resources / "non-compatible", sources = Seq("hope/this/path/does/not/exist")) + ) + } + } + + test("settings") { + + test("format") { + + assert( + testJava(resources / "non-compatible", "plain", violations = violations), + testJava(resources / "non-compatible", "sarif", violations = violations), + testJava(resources / "non-compatible", "xml", violations = violations), + testJava(resources / "compatible-java", "plain"), + testJava(resources / "compatible-java", "sarif"), + testJava(resources / "compatible-java", "xml"), + testScala(resources / "compatible-scala", "plain"), + testScala(resources / "compatible-scala", "sarif"), + testScala(resources / "compatible-scala", "xml") + ) + } + + test("options") { + + assert( + testJava(resources / "compatible-java", options = Seq("-d")) + ) + } + + test("version") { + + assert( + testJava(resources / "compatible-java", "plain", "6.3"), + testJava(resources / "compatible-java", "sarif", "8.43"), + testJava(resources / "compatible-java", "xml", "6.3") + ) + + intercept[UnsupportedOperationException]( + testJava(resources / "compatible-java", "sarif", "8.42") + ) + } + } + + test("limitations") { + + test("incompatible version generates report with unexpected violation") { + assert( + testJava( + resources / "compatible-java", + "plain", + "6.2", + violations = Seq("File not found") + ), + testJava( + resources / "compatible-java", + "xml", + "6.2", + violations = Seq("File not found") + ) + ) + } + + test("cannot set options for legacy version") { + intercept[UnsupportedOperationException]( + testJava(resources / "compatible-java", version = "6.3", options = Seq("-d")) + ) + } + } + } + + def testJava( + modulePath: os.Path, + format: String = "xml", + version: String = "10.18.1", + options: Seq[String] = Nil, + violations: Seq[String] = Seq.empty, + check: Boolean = false, + stdout: Boolean = false, + sources: Seq[String] = Seq.empty + ): Boolean = { + + object module extends TestBaseModule with JavaModule with CheckstyleModule { + override def checkstyleFormat: T[String] = format + override def checkstyleOptions: T[Seq[String]] = options + override def checkstyleVersion: T[String] = version + } + + testModule( + module, + modulePath, + violations, + CheckstyleArgs(check, stdout, Leftover(sources: _*)) + ) + } + + def testScala( + modulePath: os.Path, + format: String = "xml", + version: String = "10.18.1", + options: Seq[String] = Nil, + violations: Seq[String] = Seq.empty, + check: Boolean = false, + stdout: Boolean = false, + sources: Seq[String] = Seq.empty + ): Boolean = { + + object module extends TestBaseModule with ScalaModule with CheckstyleModule { + override def checkstyleFormat: T[String] = format + override def checkstyleOptions: T[Seq[String]] = options + override def checkstyleVersion: T[String] = version + override def scalaVersion: T[String] = sys.props("MILL_SCALA_2_13_VERSION") + } + + testModule( + module, + modulePath, + violations, + CheckstyleArgs(check, stdout, Leftover(sources: _*)) + ) + } + + def testModule( + module: TestBaseModule with CheckstyleModule, + modulePath: os.Path, + violations: Seq[String], + args: CheckstyleArgs + ): Boolean = { + val eval = UnitTester(module, modulePath) + + eval(module.checkstyle(args)).fold( + { + case api.Result.Exception(cause, _) => throw cause + case failure => throw failure + }, + numViolations => { + + numViolations.value == violations.length && { + + val Right(report) = eval(module.checkstyleOutput) + + if (os.exists(report.value.path)) + violations.isEmpty || { + val lines = os.read.lines(report.value.path) + violations.forall(violation => lines.exists(_.contains(violation))) + } + else + args.stdout + } + } + ) + } +} diff --git a/contrib/checkstyle/test/src/mill/contrib/checkstyle/CheckstyleXsltModuleTest.scala b/contrib/checkstyle/test/src/mill/contrib/checkstyle/CheckstyleXsltModuleTest.scala new file mode 100644 index 00000000000..b869d4a084b --- /dev/null +++ b/contrib/checkstyle/test/src/mill/contrib/checkstyle/CheckstyleXsltModuleTest.scala @@ -0,0 +1,63 @@ +package mill +package contrib.checkstyle + +import mainargs.Leftover +import mill.scalalib.{JavaModule, ScalaModule} +import mill.testkit.{TestBaseModule, UnitTester} +import utest._ + +object CheckstyleXsltModuleTest extends TestSuite { + + def tests: Tests = Tests { + + val resources: os.Path = os.Path(sys.env("MILL_TEST_RESOURCE_FOLDER")) + + test("checkstyle generates XSLT output reports") { + + assert( + testJava(resources / "non-compatible"), + testScala(resources / "compatible-scala") + ) + } + + test("checkstyle succeeds when no XSLT files are found") { + + assert( + testJava(resources / "compatible-java") + ) + } + } + + def testJava(modulePath: os.Path): Boolean = { + + object module extends TestBaseModule with JavaModule with CheckstyleXsltModule + + testModule(module, modulePath) + } + + def testScala(modulePath: os.Path): Boolean = { + + object module extends TestBaseModule with ScalaModule with CheckstyleXsltModule { + override def scalaVersion: T[String] = sys.props("MILL_SCALA_2_13_VERSION") + } + + testModule(module, modulePath) + } + + def testModule(module: TestBaseModule with CheckstyleXsltModule, modulePath: os.Path): Boolean = { + val eval = UnitTester(module, modulePath) + + eval(module.checkstyle(CheckstyleArgs(sources = Leftover()))).fold( + { + case api.Result.Exception(cause, _) => throw cause + case failure => throw failure + }, + _ => { + + val Right(reports) = eval(module.checkstyleXsltReports) + + reports.value.forall(report => os.exists(report.output.path)) + } + ) + } +} diff --git a/contrib/package.mill b/contrib/package.mill index 3c5ee5bda6b..9dd33ac0bb7 100644 --- a/contrib/package.mill +++ b/contrib/package.mill @@ -235,4 +235,9 @@ object `package` extends RootModule { def buildInfoObjectName = "BuildInfo" def buildInfoMembers = Seq(BuildInfo.Value("errorProneVersion", Deps.RuntimeDeps.errorProneCore.version)) } + + object checkstyle extends ContribModule { + def compileModuleDeps = Seq(build.scalalib) + def testModuleDeps = super.testModuleDeps ++ Seq(build.scalalib) + } } diff --git a/main/util/src/mill/util/Jvm.scala b/main/util/src/mill/util/Jvm.scala index b026adabed8..ed39e68613d 100644 --- a/main/util/src/mill/util/Jvm.scala +++ b/main/util/src/mill/util/Jvm.scala @@ -24,7 +24,8 @@ object Jvm extends CoursierSupport { envArgs: Map[String, String] = Map.empty, mainArgs: Seq[String] = Seq.empty, workingDir: os.Path = null, - streamOut: Boolean = true + streamOut: Boolean = true, + check: Boolean = true )(implicit ctx: Ctx): CommandResult = { val commandArgs = @@ -36,7 +37,23 @@ object Jvm extends CoursierSupport { val workingDir1 = Option(workingDir).getOrElse(ctx.dest) os.makeDir.all(workingDir1) - os.proc(commandArgs).call(cwd = workingDir1, env = envArgs) + os.proc(commandArgs).call(cwd = workingDir1, env = envArgs, check = check) + } + + /** + * Runs a JVM subprocess with the given configuration and returns a + * [[os.CommandResult]] with it's aggregated output and error streams + */ + def callSubprocess( + mainClass: String, + classPath: Agg[os.Path], + jvmArgs: Seq[String], + envArgs: Map[String, String], + mainArgs: Seq[String], + workingDir: os.Path, + streamOut: Boolean + )(implicit ctx: Ctx): CommandResult = { + callSubprocess(mainClass, classPath, jvmArgs, envArgs, mainArgs, workingDir, streamOut, true) } /**