All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sksamuel.scapegoat.io.GitlabCodeQualityReportWriter.scala Maven / Gradle / Ivy

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
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy