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

Add Markdown report #4858

Merged
merged 10 commits into from Jun 14, 2022
2 changes: 2 additions & 0 deletions detekt-generator/build.gradle.kts
Expand Up @@ -10,10 +10,12 @@ dependencies {
implementation(projects.detektRulesEmpty)
implementation(projects.detektFormatting)
implementation(projects.detektCli)
implementation(projects.detektTooling)
implementation(libs.jcommander)

testImplementation(projects.detektCore)
testImplementation(projects.detektTestUtils)
testImplementation(projects.detektTooling)
VitalyVPinchuk marked this conversation as resolved.
Show resolved Hide resolved
testImplementation(libs.assertj)
testImplementation(libs.reflections)
}
Expand Down
@@ -1,10 +1,10 @@
package io.gitlab.arturbosch.detekt.generator

import io.github.detekt.tooling.out.yaml
import io.gitlab.arturbosch.detekt.generator.collection.RuleSetPage
import io.gitlab.arturbosch.detekt.generator.out.MarkdownWriter
import io.gitlab.arturbosch.detekt.generator.out.PropertiesWriter
import io.gitlab.arturbosch.detekt.generator.out.YamlWriter
import io.gitlab.arturbosch.detekt.generator.out.yaml
import io.gitlab.arturbosch.detekt.generator.printer.DeprecatedPrinter
import io.gitlab.arturbosch.detekt.generator.printer.RuleSetPagePrinter
import io.gitlab.arturbosch.detekt.generator.printer.defaultconfig.ConfigPrinter
Expand Down
@@ -1,14 +1,14 @@
package io.gitlab.arturbosch.detekt.generator.printer

import io.github.detekt.tooling.out.bold
import io.github.detekt.tooling.out.code
import io.github.detekt.tooling.out.crossOut
import io.github.detekt.tooling.out.description
import io.github.detekt.tooling.out.h4
import io.github.detekt.tooling.out.item
import io.github.detekt.tooling.out.list
import io.github.detekt.tooling.out.markdown
import io.gitlab.arturbosch.detekt.generator.collection.Configuration
import io.gitlab.arturbosch.detekt.generator.out.bold
import io.gitlab.arturbosch.detekt.generator.out.code
import io.gitlab.arturbosch.detekt.generator.out.crossOut
import io.gitlab.arturbosch.detekt.generator.out.description
import io.gitlab.arturbosch.detekt.generator.out.h4
import io.gitlab.arturbosch.detekt.generator.out.item
import io.gitlab.arturbosch.detekt.generator.out.list
import io.gitlab.arturbosch.detekt.generator.out.markdown

internal object RuleConfigurationPrinter : DocumentationPrinter<List<Configuration>> {

Expand Down
@@ -1,14 +1,14 @@
package io.gitlab.arturbosch.detekt.generator.printer

import io.github.detekt.tooling.out.MarkdownContent
import io.github.detekt.tooling.out.bold
import io.github.detekt.tooling.out.codeBlock
import io.github.detekt.tooling.out.h3
import io.github.detekt.tooling.out.h4
import io.github.detekt.tooling.out.markdown
import io.github.detekt.tooling.out.paragraph
import io.gitlab.arturbosch.detekt.generator.collection.Active
import io.gitlab.arturbosch.detekt.generator.collection.Rule
import io.gitlab.arturbosch.detekt.generator.out.MarkdownContent
import io.gitlab.arturbosch.detekt.generator.out.bold
import io.gitlab.arturbosch.detekt.generator.out.codeBlock
import io.gitlab.arturbosch.detekt.generator.out.h3
import io.gitlab.arturbosch.detekt.generator.out.h4
import io.gitlab.arturbosch.detekt.generator.out.markdown
import io.gitlab.arturbosch.detekt.generator.out.paragraph

internal object RulePrinter : DocumentationPrinter<Rule> {

Expand Down
@@ -1,8 +1,8 @@
package io.gitlab.arturbosch.detekt.generator.printer

import io.github.detekt.tooling.out.markdown
import io.github.detekt.tooling.out.paragraph
import io.gitlab.arturbosch.detekt.generator.collection.RuleSetPage
import io.gitlab.arturbosch.detekt.generator.out.markdown
import io.gitlab.arturbosch.detekt.generator.out.paragraph

object RuleSetPagePrinter : DocumentationPrinter<RuleSetPage> {

Expand Down
@@ -1,7 +1,7 @@
package io.gitlab.arturbosch.detekt.generator.printer.defaultconfig

import io.github.detekt.tooling.out.yaml
import io.gitlab.arturbosch.detekt.generator.collection.RuleSetPage
import io.gitlab.arturbosch.detekt.generator.out.yaml
import io.gitlab.arturbosch.detekt.generator.printer.DocumentationPrinter

object ConfigPrinter : DocumentationPrinter<List<RuleSetPage>> {
Expand Down
@@ -1,14 +1,14 @@
package io.gitlab.arturbosch.detekt.generator.printer.defaultconfig

import io.github.detekt.tooling.out.YamlNode
import io.github.detekt.tooling.out.keyValue
import io.github.detekt.tooling.out.list
import io.github.detekt.tooling.out.node
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.generator.collection.Configuration
import io.gitlab.arturbosch.detekt.generator.collection.Rule
import io.gitlab.arturbosch.detekt.generator.collection.RuleSetPage
import io.gitlab.arturbosch.detekt.generator.collection.RuleSetProvider
import io.gitlab.arturbosch.detekt.generator.out.YamlNode
import io.gitlab.arturbosch.detekt.generator.out.keyValue
import io.gitlab.arturbosch.detekt.generator.out.list
import io.gitlab.arturbosch.detekt.generator.out.node

internal fun YamlNode.printRuleSetPage(ruleSetPage: RuleSetPage) {
printRuleSet(ruleSetPage.ruleSet, ruleSetPage.rules)
Expand Down
@@ -1,13 +1,13 @@
package io.gitlab.arturbosch.detekt.generator.printer.defaultconfig

import io.github.detekt.tooling.out.yaml
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.generator.collection.Active
import io.gitlab.arturbosch.detekt.generator.collection.Configuration
import io.gitlab.arturbosch.detekt.generator.collection.DefaultValue
import io.gitlab.arturbosch.detekt.generator.collection.Inactive
import io.gitlab.arturbosch.detekt.generator.collection.Rule
import io.gitlab.arturbosch.detekt.generator.collection.RuleSetProvider
import io.gitlab.arturbosch.detekt.generator.out.yaml
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
Expand Down
13 changes: 13 additions & 0 deletions detekt-report-md/build.gradle.kts
@@ -0,0 +1,13 @@
plugins {
id("module")
}

dependencies {
implementation(projects.detektMetrics)
implementation(projects.detektApi)
implementation(projects.detektTooling)

testImplementation(testFixtures(projects.detektApi))
testImplementation(libs.mockk)
testImplementation(libs.assertj)
}
@@ -0,0 +1,173 @@
package io.github.detekt.report.md

import io.github.detekt.metrics.ComplexityReportGenerator
import io.github.detekt.psi.toUnifiedString
import io.github.detekt.tooling.out.MarkdownContent
import io.github.detekt.tooling.out.codeBlock
import io.github.detekt.tooling.out.emptyLine
import io.github.detekt.tooling.out.h1
import io.github.detekt.tooling.out.h2
import io.github.detekt.tooling.out.h3
import io.github.detekt.tooling.out.item
import io.github.detekt.tooling.out.list
import io.github.detekt.tooling.out.markdown
import io.github.detekt.tooling.out.paragraph
import io.gitlab.arturbosch.detekt.api.Detektion
import io.gitlab.arturbosch.detekt.api.Finding
import io.gitlab.arturbosch.detekt.api.OutputReport
import io.gitlab.arturbosch.detekt.api.ProjectMetric
import io.gitlab.arturbosch.detekt.api.SourceLocation
import io.gitlab.arturbosch.detekt.api.internal.whichDetekt
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Locale
import kotlin.math.max
import kotlin.math.min

private const val DETEKT_WEBSITE_BASE_URL = "https://detekt.dev"

private const val EXTRA_LINES_IN_SNIPPET = 3

/**
* Contains rule violations in Markdown format report.
* [See](https://detekt.dev/docs/introduction/configurations/#output-reports)
*/
class MdOutputReport : OutputReport() {
override val ending: String = "md"

override val name = "Markdown report"

override fun render(detektion: Detektion) = markdown {
h1 { "detekt" }

h2 { "Metrics" }
renderMetrics(detektion.metrics)

h2 { "Complexity Report" }
renderComplexity(getComplexityMetrics(detektion))

renderFindings(detektion.findings)
emptyLine()

paragraph {
val detektLink = link("detekt version ${renderVersion()}", "$DETEKT_WEBSITE_BASE_URL/")
"generated with $detektLink on ${renderDate()}"
}
}

private fun renderVersion(): String = whichDetekt() ?: "unknown"

private fun renderDate(): String {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
return "${OffsetDateTime.now(ZoneOffset.UTC).format(formatter)} UTC"
}

private fun getComplexityMetrics(detektion: Detektion): List<String> {
return ComplexityReportGenerator.create(detektion).generate().orEmpty()
}
}

private fun MarkdownContent.renderMetrics(metrics: Collection<ProjectMetric>) {
list {
metrics.forEach { item { "%,d ${it.type}".format(Locale.US, it.value) } }
}
}

private fun MarkdownContent.renderComplexity(complexityReport: List<String>) {
list {
complexityReport.forEach { item { it.trim() } }
}
}

private fun MarkdownContent.renderGroup(group: String, findings: List<Finding>) {
findings
.groupBy { it.id }
.toList()
.sortedBy { (rule, _) -> rule }
.forEach { (rule, ruleFindings) ->
renderRule(rule, group, ruleFindings)
}
}

private fun MarkdownContent.renderRule(rule: String, group: String, findings: List<Finding>) {
h3 { "$group, $rule (%,d)".format(Locale.US, findings.size) }
paragraph { (findings.first().issue.description) }

paragraph {
link(
"Documentation",
"$DETEKT_WEBSITE_BASE_URL/docs/rules/${group.toLowerCase(Locale.US)}#${rule.toLowerCase(Locale.US)}"
)
}

list {
findings
.sortedWith(compareBy({ it.file }, { it.location.source.line }, { it.location.source.column }))
.forEach {
item { renderFinding(it) }
}
}
}

private fun MarkdownContent.renderFindings(findings: Map<String, List<Finding>>) {
val total = findings.values
.asSequence()
.map { it.size }
.fold(0) { a, b -> a + b }

h2 { "Findings (%,d)".format(Locale.US, total) }

findings
.filter { it.value.isNotEmpty() }
.toList()
.sortedBy { (group, _) -> group }
.forEach { (group, groupFindings) ->
renderGroup(group, groupFindings)
}
}

private fun MarkdownContent.renderFinding(finding: Finding): String {
val filePath = finding.location.filePath.relativePath ?: finding.location.filePath.absolutePath
val location = "${filePath.toUnifiedString()}:${finding.location.source.line}:${finding.location.source.column}"

val message = if (finding.message.isNotEmpty()) {
codeBlock("") { finding.message }
} else { "" }

val psiFile = finding.entity.ktElement?.containingFile
val snippet = if (psiFile != null) {
val lineSequence = psiFile.text.splitToSequence('\n')
snippetCode(lineSequence, finding.startPosition)
} else { "" }

return "$location\n$message\n$snippet"
}

private fun MarkdownContent.snippetCode(lines: Sequence<String>, location: SourceLocation): String {
val dropLineCount = max(location.line - 1 - EXTRA_LINES_IN_SNIPPET, 0)
val takeLineCount = EXTRA_LINES_IN_SNIPPET + 1 + min(location.line - 1, EXTRA_LINES_IN_SNIPPET)
var currentLineNumber = dropLineCount + 1
var text = ""

val lineNoSpace = (currentLineNumber + takeLineCount).toString().length

lines
.drop(dropLineCount)
.take(takeLineCount)
.forEach { line ->
val lineNo = ("$currentLineNumber ").take(lineNoSpace)
text += "$lineNo $line\n"

if (currentLineNumber == location.line) {
val positions = currentLineNumber.toString().length
val lineErr = "!".repeat(positions) + " ".repeat(location.column + lineNoSpace - positions)
text += "$lineErr^ error\n"
}
currentLineNumber++
}

return codeBlock("kotlin") { text }
}

internal fun MarkdownContent.link(text: String, url: String) = "[$text]($url)"
@@ -0,0 +1 @@
io.github.detekt.report.txt.MdOutputReport