Skip to content

Commit

Permalink
Support GitLab Code Quality report format
Browse files Browse the repository at this point in the history
  • Loading branch information
Johnnei committed Nov 23, 2023
1 parent 9f33cec commit df31199
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 8 deletions.
19 changes: 12 additions & 7 deletions src/main/scala/com/sksamuel/scapegoat/Configuration.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ case class Reports(
disableXML: Boolean,
disableHTML: Boolean,
disableScalastyleXML: Boolean,
disableMarkdown: Boolean
disableMarkdown: Boolean,
disableGitlabCodeQuality: Boolean
)

case class Configuration(
Expand Down Expand Up @@ -50,11 +51,14 @@ object Configuration {
.map(inspection => Class.forName(inspection).getConstructor().newInstance().asInstanceOf[Inspection])
}
val enabledReports = fromProperty("reports", defaultValue = Seq("all"))(_.split(':').toSeq)
val disableXML = !(enabledReports.contains("xml") || enabledReports.contains("all"))
val disableHTML = !(enabledReports.contains("html") || enabledReports.contains("all"))
def isReportEnabled(report: String): Boolean =
enabledReports.contains(report) || enabledReports.contains("all")
val disableXML = !isReportEnabled("xml")
val disableHTML = !isReportEnabled("html")
val disableScalastyleXML =
!(enabledReports.contains("scalastyle") || enabledReports.contains("all"))
val disableMarkdown = !(enabledReports.contains("markdown") || enabledReports.contains("all"))
!isReportEnabled("scalastyle")
val disableMarkdown = !isReportEnabled("markdown")
val disableGitlabCodeQuality = !isReportEnabled("gitlab-codequality")

val levelOverridesByInspectionSimpleName =
fromProperty("overrideLevels", defaultValue = Map.empty[String, Level]) {
Expand Down Expand Up @@ -97,7 +101,8 @@ object Configuration {
disableXML = disableXML,
disableHTML = disableHTML,
disableScalastyleXML = disableScalastyleXML,
disableMarkdown = disableMarkdown
disableMarkdown = disableMarkdown,
disableGitlabCodeQuality = disableGitlabCodeQuality
),
customInspectors = customInspectors,
sourcePrefix = sourcePrefix,
Expand All @@ -118,7 +123,7 @@ object Configuration {
"-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', 'markdown',",
" or `all'. Use `none' to disable reports.",
" 'gilab-codequality' 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.",
" <levels> should be a colon separated list of name=level",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.sksamuel.scapegoat.io
import com.sksamuel.scapegoat.{Feedback, Levels, Warning}

import java.nio.charset.StandardCharsets
import java.security.MessageDigest

/**
* Supports GitLab Code Quality report format.
*
* https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
*/
object GitlabCodeQualityReportWriter extends ReportWriter {

override protected def fileName: String = "scapegoat-gitlab.json"

override protected def generate(feedback: Feedback): String =
toCodeQualityElements(feedback.warningsWithMinimalLevel, sys.env.get("CI_PROJECT_DIR"))
.map(_.toJsonArrayElement)
.mkString("[", ",", "]")

def toCodeQualityElements(
warnings: Seq[Warning],
gitlabBuildDir: Option[String]
): Seq[CodeQualityReportElement] = warnings.map { warning =>
// Stable hash for the same warning.
// Avoids moving code blocks around from causing "new" detecions.
val fingerprintRaw = warning.sourceFileNormalized + warning.snippet.getOrElse(warning.line.toString)
val fingerprint = MessageDigest
.getInstance("MD5")
.digest(fingerprintRaw.getBytes(StandardCharsets.UTF_8))
.map("%02x".format(_))
.mkString

val severity = warning.level match {
case Levels.Error => CriticalSeverity
case Levels.Warning => MinorSeverity
case Levels.Info => InfoSeverity
case _ => InfoSeverity
}

val gitlabCiNormalizedPath = gitlabBuildDir
.map { buildDir =>
val fullBuildDir = if (buildDir.endsWith("/")) buildDir else s"$buildDir/"
val file = warning.sourceFileFull
if (file.startsWith(fullBuildDir)) file.drop(fullBuildDir.length) else file
}
.getOrElse(warning.sourceFileFull)

val textStart = if (warning.explanation.startsWith(warning.text)) {
""
} else {
if (warning.text.endsWith(".")) {
warning.text + " "
} else {
warning.text + ". "
}
}
val description = s"$textStart${warning.explanation}"

CodeQualityReportElement(
description = description,
checkName = warning.inspection,
severity = severity,
location = Location(gitlabCiNormalizedPath, Lines(warning.line)),
fingerprint = fingerprint
)
}
}

sealed trait CodeClimateSeverity {
val name: String
}

case object InfoSeverity extends CodeClimateSeverity {
override val name: String = "info"
}

case object MinorSeverity extends CodeClimateSeverity {
override val name: String = "minor"
}

case object CriticalSeverity extends CodeClimateSeverity {
override val name: String = "critical"
}

case class Location(path: String, lines: Lines)

case class Lines(begin: Int)

case class CodeQualityReportElement(
description: String,
checkName: String,
severity: CodeClimateSeverity,
location: Location,
fingerprint: String
) {

// Manual templating is a bit silly but avoids a dependency on a potentially conflicting json library.
def toJsonArrayElement: String =
s"""
| {
| "description": "${description.replace("\"", "\\\"")}",
| "check_name": "$checkName",
| "fingerprint": "$fingerprint",
| "severity": "${severity.name}",
| "location": {
| "path": "${location.path}",
| "lines": {
| "begin": ${location.lines.begin}
| }
| }
| }""".stripMargin
}
3 changes: 3 additions & 0 deletions src/main/scala/com/sksamuel/scapegoat/io/IOUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ object IOUtils {

def writeMarkdownReport(targetDir: File, reporter: Feedback): File =
MarkdownReportWriter.write(targetDir, reporter)

def writeGitlabCodeQualityReport(targetDir: File, reporter: Feedback): File =
GitlabCodeQualityReportWriter.write(targetDir, reporter)
}
5 changes: 5 additions & 0 deletions src/main/scala/com/sksamuel/scapegoat/plugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ class ScapegoatComponent(val global: Global, inspections: Seq[Inspection])
writeReport(reports.disableXML, "XML", IOUtils.writeXMLReport)
writeReport(reports.disableScalastyleXML, "Scalastyle XML", IOUtils.writeScalastyleReport)
writeReport(reports.disableMarkdown, "Markdown", IOUtils.writeMarkdownReport)
writeReport(
reports.disableGitlabCodeQuality,
"GitLab Code Quality",
IOUtils.writeGitlabCodeQualityReport
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ object TestConfiguration {
disableXML = true,
disableHTML = true,
disableScalastyleXML = true,
disableMarkdown = true
disableMarkdown = true,
disableGitlabCodeQuality = true
),
customInspectors = Seq(),
sourcePrefix = "src/main/scala",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.sksamuel.scapegoat.io

import com.sksamuel.scapegoat.{Levels, Warning}
import org.scalatest.freespec.AnyFreeSpec
import org.scalatest.matchers.should.Matchers

class GitlabCodeQualityReportWriterTest extends AnyFreeSpec with Matchers {

"GitlabCodeQualityReportWriter" - {

"should transform feedback" in {
val warnings = Seq(
Warning(
"Use of Option.get",
13,
Levels.Error,
"/home/johnnei/git/scapegoat/src/main/scala/com/sksamuel/File.scala",
"com.sksamuel.File.scala",
Some("File.this.d.get"),
"Using Option.get defeats the purpose",
"com.sksamuel.scapegoat.inspections.option.OptionGet"
)
)

val report = GitlabCodeQualityReportWriter
.toCodeQualityElements(warnings, Some("/home/johnnei/git/scapegoat"))
report should be(
Seq(
CodeQualityReportElement(
"Use of Option.get. Using Option.get defeats the purpose",
"com.sksamuel.scapegoat.inspections.option.OptionGet",
CriticalSeverity,
Location("src/main/scala/com/sksamuel/File.scala", Lines(13)),
"909b14c15a3a3891659251f133058264"
)
)
)
}

"should transform feedback without duplicate text" in {
val warnings = Seq(
Warning(
"List.size is O(n)",
13,
Levels.Info,
"/home/johnnei/git/scapegoat/src/main/scala/com/sksamuel/File.scala",
"com.sksamuel.File.scala",
None,
"List.size is O(n). Consider using...",
"com.sksamuel.scapegoat.inspections.collections.ListSize"
)
)

val report = GitlabCodeQualityReportWriter
.toCodeQualityElements(warnings, Some("/home/johnnei/git/scapegoat"))
report should be(
Seq(
CodeQualityReportElement(
"List.size is O(n). Consider using...",
"com.sksamuel.scapegoat.inspections.collections.ListSize",
InfoSeverity,
Location("src/main/scala/com/sksamuel/File.scala", Lines(13)),
"f79bc3223909939407272a1db37a6d17"
)
)
)
}
}

}

0 comments on commit df31199

Please sign in to comment.