Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support GitLab Code Quality report format #803

Merged
merged 2 commits into from
Nov 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,19 +110,19 @@ You can pass other configuration flags adding it to the `additionalParameters` l

## Full list of compiler flags

| Flag | Parameters | Required |
|-------------------------------------|---------------------------------------------------------------------------------------------------------------------------|----------|
| `-P:scapegoat:dataDir:` | Path to reports directory for the plugin. | true |
| `-P:scapegoat:disabledInspections:` | Colon separated list of disabled inspections (defaults to none). | false |
| `-P:scapegoat:enabledInspections:` | Colon separated list of enabled inspections (defaults to all). | false |
| `-P:scapegoat:customInspectors:` | Colon separated list of custom inspections. | false |
| `-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`, `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:minimalLevel:` | Provides minimal level of inspection displayed in reports and in the console. | false |
| Flag | Parameters | Required |
|-------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|----------|
| `-P:scapegoat:dataDir:` | Path to reports directory for the plugin. | true |
| `-P:scapegoat:disabledInspections:` | Colon separated list of disabled inspections (defaults to none). | false |
| `-P:scapegoat:enabledInspections:` | Colon separated list of enabled inspections (defaults to all). | false |
| `-P:scapegoat:customInspectors:` | Colon separated list of custom inspections. | false |
| `-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`, `markdown`, ,`gitlab-codequality`, 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:minimalLevel:` | Provides minimal level of inspection displayed in reports and in the console. | false |

## Reports

Expand Down
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.",
" 'gitlab-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,119 @@
package com.sksamuel.scapegoat.io

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

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

/**
* 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 = {
val md5Digest = MessageDigest.getInstance("MD5")
toCodeQualityElements(feedback.warningsWithMinimalLevel, sys.env.get("CI_PROJECT_DIR"), md5Digest)
.map(_.toJsonArrayElement)
.mkString("[", ",", "]")
}

private[io] def toCodeQualityElements(
warnings: Seq[Warning],
gitlabBuildDir: Option[String],
messageDigest: MessageDigest
): 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)

messageDigest.reset()
messageDigest.update(fingerprintRaw.getBytes(StandardCharsets.UTF_8))
val fingerprint = messageDigest
.digest()
.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"
}

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

final case class Lines(begin: Int)

final 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,66 @@
package com.sksamuel.scapegoat.io

import java.security.MessageDigest

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"
),
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"),
MessageDigest.getInstance("MD5")
)
report should be(
Seq(
CodeQualityReportElement(
// Extra dot after warning text to improve readability
"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"
),
CodeQualityReportElement(
// Warning text is trimmed to avoid duplicate text
"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"
)
)
)
}
}

}
Loading