Skip to content

Commit

Permalink
Improved reports, markdown report (#376)
Browse files Browse the repository at this point in the history
* draft interface

* refactor reporters

* markdown reporter

* clean up report execution

* readme and accidental changes

* scalafix
  • Loading branch information
eugene-sy authored Jun 12, 2020
1 parent 4997198 commit b99ee39
Show file tree
Hide file tree
Showing 12 changed files with 123 additions and 69 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ You can pass other configuration flags adding it to the `additionalParameters` l
|`-P:scapegoat:ignoredFiles:`|Colon separated list of regexes to match files to ignore.|false|
|`-P:scapegoat:verbose:`|Boolean flag that enables/disables verbose console messages.|false|
|`-P:scapegoat:consoleOutput:`|Boolean flag that enables/disables console report output.|false|
|`-P:scapegoat:reports:`|Colon separated list of reports to generate. Valid options are `none`, `xml`, `html`, `scalastyle`, or `all`.|false|
|`-P:scapegoat:reports:`|Colon separated list of reports to generate. Valid options are `none`, `xml`, `html`, `scalastyle`, `markdown`, or `all`.|false|
|`-P:scapegoat:overrideLevels:`|Overrides the built in warning levels. Should be a colon separated list of `name=level` expressions.|false|
|`-P:scapegoat:sourcePrefix:`|Overrides source prefix if it differs from `src/main/scala`, for ex. `app/` for Play applications.|false|
|`-P:scapegoat:minimalWarnLevel:`|Provides minimal level of inspection displayed in reports.|false|
Expand Down
16 changes: 10 additions & 6 deletions src/main/scala/com/sksamuel/scapegoat/io/HtmlReportWriter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import scala.xml.Unparsed
import com.sksamuel.scapegoat.{Feedback, Levels}

/** @author Stephen Samuel */
object HtmlReportWriter {
object HtmlReportWriter extends ReportWriter {

val css =
override protected def fileName: String = "scapegoat.html"

private val css =
"""
| body {
| font-family: 'Ubuntu', sans-serif;
Expand Down Expand Up @@ -60,7 +62,7 @@ object HtmlReportWriter {
|
""".stripMargin

def header =
private def header =
<head>
<title>Scapegoat Inspection Reporter</title>{
Unparsed(
Expand All @@ -79,7 +81,7 @@ object HtmlReportWriter {
</style>
</head>

def body(reporter: Feedback) =
private def body(reporter: Feedback) =
<body>
<h1>Scapegoat Inspections</h1>
<h3>
Expand All @@ -92,7 +94,7 @@ object HtmlReportWriter {
</h3>{warnings(reporter)}
</body>

def warnings(reporter: Feedback) = {
private def warnings(reporter: Feedback) = {
reporter.warningsWithMinimalLevel.map { warning =>
val source = warning.sourceFileNormalized + ":" + warning.line
<div class="warning">
Expand Down Expand Up @@ -125,8 +127,10 @@ object HtmlReportWriter {
}
}

def generate(reporter: Feedback) =
private def toHTML(reporter: Feedback) =
<html>
{header}{body(reporter)}
</html>

override protected def generate(feedback: Feedback): String = toHTML(feedback).toString()
}
41 changes: 9 additions & 32 deletions src/main/scala/com/sksamuel/scapegoat/io/IOUtils.scala
Original file line number Diff line number Diff line change
@@ -1,45 +1,22 @@
package com.sksamuel.scapegoat.io

import java.io.{BufferedWriter, File, FileWriter}
import java.io.File

import com.sksamuel.scapegoat.Feedback

/**
* @author Stephen Samuel
* @author Eugene Sypachev (Axblade)
*/
object IOUtils {
def writeHTMLReport(targetDir: File, reporter: Feedback): File =
HtmlReportWriter.write(targetDir, reporter)

private val XmlFile = "scapegoat.xml"
private val ScalastyleXmlFile = "scapegoat-scalastyle.xml"
private val HtmlFile = "scapegoat.html"
def writeXMLReport(targetDir: File, reporter: Feedback): File =
XmlReportWriter.write(targetDir, reporter)

def serialize(file: File, str: String) = {
val out = new BufferedWriter(new FileWriter(file))
out.write(str)
out.close()
}

def writeHTMLReport(targetDir: File, reporter: Feedback): File = {
val html = HtmlReportWriter.generate(reporter).toString()
writeFile(targetDir, html, HtmlFile)
}

def writeXMLReport(targetDir: File, reporter: Feedback): File = {
val xml = XmlReportWriter.toXML(reporter).toString()
writeFile(targetDir, xml, XmlFile)
}

def writeScalastyleReport(targetDir: File, reporter: Feedback): File = {
val xml = ScalastyleReportWriter.toXML(reporter).toString()
writeFile(targetDir, xml, ScalastyleXmlFile)
}

private def writeFile(targetDir: File, data: String, fileName: String) = {
targetDir.mkdirs()
val file = new File(targetDir.getAbsolutePath + "/" + fileName)
serialize(file, data)
file
}
def writeScalastyleReport(targetDir: File, reporter: Feedback): File =
ScalastyleReportWriter.write(targetDir, reporter)

def writeMarkdownReport(targetDir: File, reporter: Feedback): File =
MarkdownReportWriter.write(targetDir, reporter)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.sksamuel.scapegoat.io

import com.sksamuel.scapegoat.{Feedback, Levels, Warning}

object MarkdownReportWriter extends ReportWriter {
override protected def fileName: String = "scapegoat.md"

override protected def generate(reporter: Feedback): String = {
s"""# Scapegoat Inspections
|
|**Errors**: ${reporter.warnings(Levels.Error).size.toString}
|
|**Warnings**: ${reporter.warnings(Levels.Warning).size.toString}
|
|**Infos**: ${reporter.warnings(Levels.Info).size.toString}
|
|## Report
|
|${renderAll(reporter)}
|""".stripMargin
}

private def renderAll(reporter: Feedback): String =
reporter.warningsWithMinimalLevel.map(renderWarning).mkString("\n")

private def renderWarning(warning: Warning): String = {
val source = warning.sourceFileNormalized + ":" + warning.line
val md =
s"""### $source
|
|**Level**: ${warning.level.toString}
|
|**Inspection**: ${warning.inspection}
|
|${warning.text}
|
|${warning.explanation}
|
|${warning.snippet.map(snippet => s"\n```scala\n$snippet\n```").getOrElse("")}
|""".stripMargin
md
}
}
25 changes: 25 additions & 0 deletions src/main/scala/com/sksamuel/scapegoat/io/ReportWriter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.sksamuel.scapegoat.io

import java.io.{BufferedWriter, File, FileWriter}

import com.sksamuel.scapegoat.Feedback

trait ReportWriter {

protected def fileName: String

protected def generate(feedback: Feedback): String

private def serialize(file: File, str: String): Unit = {
val out = new BufferedWriter(new FileWriter(file))
out.write(str)
out.close()
}

def write(targetDir: File, feedback: Feedback): File = {
targetDir.mkdirs()
val file = new File(s"${targetDir.getAbsolutePath}/$fileName")
serialize(file, generate(feedback))
file
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ import scala.xml.Node

import com.sksamuel.scapegoat.{Feedback, Warning}

/** @author Eugene Sypachev (Axblade) */
object ScalastyleReportWriter {
object ScalastyleReportWriter extends ReportWriter {

private val checkstyleVersion = "5.0"
private val scapegoat = "scapegoat"

def toXML(feedback: Feedback): Node = {
override protected val fileName = "scapegoat-scalastyle.xml"

private def toXML(feedback: Feedback): Node =
<checkstyle version={checkstyleVersion} generatedBy={scapegoat}>
{feedback.warningsWithMinimalLevel.groupBy(_.sourceFileFull).map(fileToXml)}
</checkstyle>
}

private def fileToXml(fileWarningMapEntry: (String, Seq[Warning])) = {
private def fileToXml(fileWarningMapEntry: (String, Seq[Warning])): Node = {
val (file, warnings) = fileWarningMapEntry
<file name={file}>
{warnings.map(warningToXml)}
Expand All @@ -28,4 +28,5 @@ object ScalastyleReportWriter {
warning.inspection
} snippet={warning.snippet.orNull} explanation={warning.explanation}></error>

override protected def generate(feedback: Feedback): String = toXML(feedback).toString()
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import scala.xml.Node
import com.sksamuel.scapegoat.{Feedback, Warning}

/** @author Stephen Samuel */
object XmlReportWriter {
object XmlReportWriter extends ReportWriter {

def toXML(feedback: Feedback): Node = {
override protected val fileName = "scapegoat.xml"

private def toXML(feedback: Feedback): Node = {
<scapegoat count={feedback.warnings.size.toString} warns={feedback.warns.size.toString} errors={
feedback.errors.size.toString
} infos={feedback.infos.size.toString}>
Expand All @@ -19,4 +21,6 @@ object XmlReportWriter {
<warning line={warning.line.toString} text={warning.text} snippet={warning.snippet.orNull} explanation={
warning.explanation
} level={warning.level.toString} file={warning.sourceFileNormalized} inspection={warning.inspection}/>

override protected def generate(feedback: Feedback): String = toXML(feedback).toString()
}
33 changes: 17 additions & 16 deletions src/main/scala/com/sksamuel/scapegoat/plugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,17 @@ class ScapegoatPlugin(val global: Global) extends Plugin {
case "xml" => component.disableXML = false
case "html" => component.disableHTML = false
case "scalastyle" => component.disableScalastyleXML = false
case "markdown" => component.disableMarkdown = false
case "all" =>
component.disableXML = false
component.disableHTML = false
component.disableScalastyleXML = false
component.disableMarkdown = false
case "none" =>
component.disableXML = true
component.disableHTML = true
component.disableScalastyleXML = true
component.disableMarkdown = true
case _ =>
}
case None =>
Expand Down Expand Up @@ -122,7 +125,7 @@ class ScapegoatPlugin(val global: Global) extends Plugin {
"-P:scapegoat:verbose:<boolean> enable/disable verbose console messages",
"-P:scapegoat:consoleOutput:<boolean> enable/disable console report output",
"-P:scapegoat:reports:<reports> colon separated list of reports to generate.",
" Valid options are `xml', `html', `scalastyle',",
" Valid options are `xml', `html', `scalastyle', 'markdown',",
" or `all'. Use `none' to disable reports.",
"-P:scapegoat:overrideLevels:<levels> override the built in warning levels, e.g. to",
" downgrade a Error to a Warning.",
Expand Down Expand Up @@ -164,6 +167,7 @@ class ScapegoatComponent(val global: Global, inspections: Seq[Inspection])
var disableXML = true
var disableHTML = true
var disableScalastyleXML = true
var disableMarkdown = true
var customInpections: Seq[Inspection] = Nil
var sourcePrefix = "src/main/scala/"
var minimalLevel: Level = Levels.Info
Expand All @@ -186,6 +190,14 @@ class ScapegoatComponent(val global: Global, inspections: Seq[Inspection])
}
lazy val feedback = new Feedback(consoleOutput, global.reporter, sourcePrefix, minimalLevel)

def writeReport(isDisabled: Boolean, reportName: String, writer: (File, Feedback) => File): Unit = {
if (!isDisabled) {
val output = writer(dataDir, feedback)
if (verbose)
reporter.echo(s"[info] [scapegoat] Written $reportName report [$output]")
}
}

override def newPhase(prev: scala.tools.nsc.Phase): Phase =
new Phase(prev) {
override def run(): Unit = {
Expand All @@ -208,21 +220,10 @@ class ScapegoatComponent(val global: Global, inspections: Seq[Inspection])
)
}

if (!disableHTML) {
val html = IOUtils.writeHTMLReport(dataDir, feedback)
if (verbose)
reporter.echo(s"[info] [scapegoat] Written HTML report [$html]")
}
if (!disableXML) {
val xml = IOUtils.writeXMLReport(dataDir, feedback)
if (verbose)
reporter.echo(s"[info] [scapegoat] Written XML report [$xml]")
}
if (!disableScalastyleXML) {
val xml = IOUtils.writeScalastyleReport(dataDir, feedback)
if (verbose)
reporter.echo(s"[info] [scapegoat] Written Scalastyle XML report [$xml]")
}
writeReport(disableHTML, "HTML", IOUtils.writeHTMLReport)
writeReport(disableXML, "XML", IOUtils.writeXMLReport)
writeReport(disableScalastyleXML, "Scalastyle XML", IOUtils.writeScalastyleReport)
writeReport(disableMarkdown, "Markdown", IOUtils.writeMarkdownReport)
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/test/scala/com/sksamuel/scapegoat/FeedbackTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ class FeedbackTest extends AnyFreeSpec with Matchers with OneInstancePerTest wit
val feedback = new Feedback(false, reporter, defaultSourcePrefix, Levels.Warning)
inspections.foreach(inspection => feedback.warn(position, inspection))
feedback.warningsWithMinimalLevel.length should be(2)
feedback.warningsWithMinimalLevel.map(_.level) should contain only (Seq(Levels.Warning, Levels.Error): _*)
feedback.warningsWithMinimalLevel
.map(_.level) should contain only (Seq(Levels.Warning, Levels.Error): _*)
}

"for `error`" in {
Expand Down
6 changes: 2 additions & 4 deletions src/test/scala/com/sksamuel/scapegoat/PluginRunner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,8 @@ trait PluginRunner {
val jarPath =
sbtHome + "/cache/" + groupId + "/" + artifactId + "/jars/" + artifactId + "-" + version + ".jar"
val file = new File(jarPath)
if (file.exists) {
// println(s"Located ivy jar [$file]")
file
} else throw new FileNotFoundException(s"Could not locate [$jarPath].")
if (file.exists) file
else throw new FileNotFoundException(s"Could not locate [$jarPath].")
}

def sbtCompileDir: File = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class FinalModifierOnCaseClassTest extends InspectionTest {
private def assertFinalModOnCaseClass(code: String): Unit = {
compileCodeSnippet(code)
compiler.scapegoat.feedback.warnings.size shouldBe 1
compiler.scapegoat.feedback.warnings.head.text shouldBe ("Missing final modifier on case class")
compiler.scapegoat.feedback.warnings.head.text shouldBe "Missing final modifier on case class"
}

private def assertNoFinalModOnCaseClass(code: String): Unit = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class MapGetAndGetOrElseTest extends InspectionTest {
private def getOrElseAssertion(code: String): Unit = {
compileCodeSnippet(code)
compiler.scapegoat.feedback.warnings.size shouldBe 1
compiler.scapegoat.feedback.warnings.head.text shouldBe ("Use of Map.get().getOrElse instead of Map.getOrElse")
compiler.scapegoat.feedback.warnings.head.text shouldBe "Use of Map.get().getOrElse instead of Map.getOrElse"
}

"Map with get followed by getOrElse" - {
Expand Down

0 comments on commit b99ee39

Please sign in to comment.