Skip to content

Commit

Permalink
Added plugins to run Checkstyle and generate reports (#3516)
Browse files Browse the repository at this point in the history
Added plugins
- `CheckstyleModule`, fully compliant with [Checkstyle
CLI](https://checkstyle.org/cmdline.html#Command_line_usage)
- `CheckstyleXsltModule`, generates reports by applying XSL
Transformations on a Checkstyle output report

Resolves #3332.
  • Loading branch information
ajaychandran authored Sep 12, 2024
1 parent 4fc6f86 commit a02a3c6
Show file tree
Hide file tree
Showing 23 changed files with 1,784 additions and 2 deletions.
128 changes: 128 additions & 0 deletions contrib/checkstyle/readme.adoc
Original file line number Diff line number Diff line change
@@ -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"),
)
)
}
}
----
Original file line number Diff line number Diff line change
@@ -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]
}
106 changes: 106 additions & 0 deletions contrib/checkstyle/src/mill/contrib/checkstyle/CheckstyleModule.scala
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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]
}
}
}
Original file line number Diff line number Diff line change
@@ -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]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN" "http://www.puppycrawl.com/dtds/configuration_1_3.dtd">

<module name="Checker">

<module name="TreeWalker">

<module name="HideUtilityClassConstructor" />

</module>

</module>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.etsy.sbt;

public class TestClass {

private TestClass() {
}

public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
Loading

0 comments on commit a02a3c6

Please sign in to comment.