diff --git a/detekt-core/build.gradle.kts b/detekt-core/build.gradle.kts index 0d92502f418..19e1355f421 100644 --- a/detekt-core/build.gradle.kts +++ b/detekt-core/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { implementation(projects.detektMetrics) implementation(projects.detektPsiUtils) implementation(projects.detektReportHtml) + implementation(projects.detektReportMd) implementation(projects.detektReportTxt) implementation(projects.detektReportXml) implementation(projects.detektReportSarif) diff --git a/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/reporting/Reporting.kt b/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/reporting/Reporting.kt index f353256ce58..9be5d9d9cc0 100644 --- a/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/reporting/Reporting.kt +++ b/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/reporting/Reporting.kt @@ -1,6 +1,7 @@ package io.gitlab.arturbosch.detekt.core.reporting import io.github.detekt.report.html.HtmlOutputReport +import io.github.detekt.report.md.MdOutputReport import io.github.detekt.report.sarif.SarifOutputReport import io.github.detekt.report.txt.TxtOutputReport import io.github.detekt.report.xml.XmlOutputReport @@ -18,6 +19,7 @@ internal fun defaultReportMapping(reportId: String) = when (reportId) { XmlOutputReport::class.java.simpleName -> "xml" HtmlOutputReport::class.java.simpleName -> "html" SarifOutputReport::class.java.simpleName -> "sarif" + MdOutputReport::class.java.simpleName -> "md" else -> reportId } diff --git a/detekt-core/src/main/resources/default-detekt-config.yml b/detekt-core/src/main/resources/default-detekt-config.yml index a5647513d66..f7da171eeb3 100644 --- a/detekt-core/src/main/resources/default-detekt-config.yml +++ b/detekt-core/src/main/resources/default-detekt-config.yml @@ -46,6 +46,7 @@ output-reports: # - 'TxtOutputReport' # - 'XmlOutputReport' # - 'HtmlOutputReport' + # - 'MdOutputReport' comments: active: true diff --git a/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/reporting/OutputFacadeSpec.kt b/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/reporting/OutputFacadeSpec.kt index 0562b374dc6..2ca6dbad183 100644 --- a/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/reporting/OutputFacadeSpec.kt +++ b/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/reporting/OutputFacadeSpec.kt @@ -1,6 +1,7 @@ package io.gitlab.arturbosch.detekt.core.reporting import io.github.detekt.report.html.HtmlOutputReport +import io.github.detekt.report.md.MdOutputReport import io.github.detekt.report.txt.TxtOutputReport import io.github.detekt.report.xml.XmlOutputReport import io.github.detekt.test.utils.StringPrintStream @@ -24,6 +25,7 @@ class OutputFacadeSpec { val plainOutputPath = createTempFileForTest("detekt", ".txt") val htmlOutputPath = createTempFileForTest("detekt", ".html") val xmlOutputPath = createTempFileForTest("detekt", ".xml") + val mdOutputPath = createTempFileForTest("detekt", ".md") val spec = createNullLoggingSpec { project { @@ -33,6 +35,7 @@ class OutputFacadeSpec { report { "html" to htmlOutputPath } report { "txt" to plainOutputPath } report { "xml" to xmlOutputPath } + report { "md" to mdOutputPath } } logging { outputChannel = printStream @@ -44,7 +47,8 @@ class OutputFacadeSpec { assertThat(printStream.toString()).contains( "Successfully generated ${TxtOutputReport().name} at $plainOutputPath", "Successfully generated ${XmlOutputReport().name} at $xmlOutputPath", - "Successfully generated ${HtmlOutputReport().name} at $htmlOutputPath" + "Successfully generated ${HtmlOutputReport().name} at $htmlOutputPath", + "Successfully generated ${MdOutputReport().name} at $mdOutputPath" ) } } diff --git a/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/reporting/OutputReportsSpec.kt b/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/reporting/OutputReportsSpec.kt index 4f8aace147f..7fdbbf25f20 100644 --- a/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/reporting/OutputReportsSpec.kt +++ b/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/reporting/OutputReportsSpec.kt @@ -1,6 +1,7 @@ package io.gitlab.arturbosch.detekt.core.reporting import io.github.detekt.report.html.HtmlOutputReport +import io.github.detekt.report.md.MdOutputReport import io.github.detekt.report.txt.TxtOutputReport import io.github.detekt.report.xml.XmlOutputReport import io.github.detekt.test.utils.resourceAsPath @@ -28,11 +29,12 @@ class OutputReportsSpec { report { "txt" to Paths.get("/tmp/path2") } report { reportUnderTest to Paths.get("/tmp/path3") } report { "html" to Paths.get("D:_Gradle\\xxx\\xxx\\build\\reports\\detekt\\detekt.html") } + report { "md" to Paths.get("/tmp/path4") } }.build().reports.toList() @Test fun `should parse multiple report entries`() { - assertThat(reports).hasSize(4) + assertThat(reports).hasSize(5) } @Test @@ -66,6 +68,13 @@ class OutputReportsSpec { ) } + @Test + fun `it should properly parse MD report entry`() { + val mdRepot = reports[4] + assertThat(mdRepot.type).isEqualTo(defaultReportMapping(MdOutputReport::class.java.simpleName)) + assertThat(mdRepot.path).isEqualTo(Paths.get("/tmp/path4")) + } + @Nested inner class `default report ids` { diff --git a/detekt-generator/build.gradle.kts b/detekt-generator/build.gradle.kts index ed37935d573..e1a96393dcb 100644 --- a/detekt-generator/build.gradle.kts +++ b/detekt-generator/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { implementation(projects.detektRulesEmpty) implementation(projects.detektFormatting) implementation(projects.detektCli) + implementation(projects.detektUtils) implementation(libs.jcommander) testImplementation(projects.detektCore) diff --git a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/DetektPrinter.kt b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/DetektPrinter.kt index 2db46a6beb6..a9dbb1e11f8 100644 --- a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/DetektPrinter.kt +++ b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/DetektPrinter.kt @@ -1,10 +1,10 @@ package io.gitlab.arturbosch.detekt.generator +import io.github.detekt.utils.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 diff --git a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/DefaultValue.kt b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/DefaultValue.kt index f3b481b2371..1fb9be76beb 100644 --- a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/DefaultValue.kt +++ b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/DefaultValue.kt @@ -1,10 +1,10 @@ package io.gitlab.arturbosch.detekt.generator.collection +import io.github.detekt.utils.YamlNode +import io.github.detekt.utils.keyValue +import io.github.detekt.utils.list +import io.github.detekt.utils.listOfMaps import io.gitlab.arturbosch.detekt.api.ValuesWithReason -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.listOfMaps sealed interface DefaultValue { diff --git a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/RuleConfigurationPrinter.kt b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/RuleConfigurationPrinter.kt index 558ab6b62b4..6ac43d2411e 100644 --- a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/RuleConfigurationPrinter.kt +++ b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/RuleConfigurationPrinter.kt @@ -1,14 +1,14 @@ package io.gitlab.arturbosch.detekt.generator.printer +import io.github.detekt.utils.bold +import io.github.detekt.utils.code +import io.github.detekt.utils.crossOut +import io.github.detekt.utils.description +import io.github.detekt.utils.h4 +import io.github.detekt.utils.item +import io.github.detekt.utils.list +import io.github.detekt.utils.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> { diff --git a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/RulePrinter.kt b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/RulePrinter.kt index 318ae70af2d..9c171d7f8e0 100644 --- a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/RulePrinter.kt +++ b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/RulePrinter.kt @@ -1,14 +1,14 @@ package io.gitlab.arturbosch.detekt.generator.printer +import io.github.detekt.utils.MarkdownContent +import io.github.detekt.utils.bold +import io.github.detekt.utils.codeBlock +import io.github.detekt.utils.h3 +import io.github.detekt.utils.h4 +import io.github.detekt.utils.markdown +import io.github.detekt.utils.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 { diff --git a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/RuleSetPagePrinter.kt b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/RuleSetPagePrinter.kt index db88a74c223..46d0f003e5b 100644 --- a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/RuleSetPagePrinter.kt +++ b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/RuleSetPagePrinter.kt @@ -1,8 +1,8 @@ package io.gitlab.arturbosch.detekt.generator.printer +import io.github.detekt.utils.markdown +import io.github.detekt.utils.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 { diff --git a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/defaultconfig/ConfigPrinter.kt b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/defaultconfig/ConfigPrinter.kt index 486e36b94bd..9131be47088 100644 --- a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/defaultconfig/ConfigPrinter.kt +++ b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/defaultconfig/ConfigPrinter.kt @@ -1,7 +1,7 @@ package io.gitlab.arturbosch.detekt.generator.printer.defaultconfig +import io.github.detekt.utils.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> { @@ -81,5 +81,6 @@ object ConfigPrinter : DocumentationPrinter> { # - 'TxtOutputReport' # - 'XmlOutputReport' # - 'HtmlOutputReport' + # - 'MdOutputReport' """.trimIndent() } diff --git a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/defaultconfig/RuleSetConfigPrinter.kt b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/defaultconfig/RuleSetConfigPrinter.kt index 3034cd417a1..e412afc64d9 100644 --- a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/defaultconfig/RuleSetConfigPrinter.kt +++ b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/defaultconfig/RuleSetConfigPrinter.kt @@ -1,13 +1,13 @@ package io.gitlab.arturbosch.detekt.generator.printer.defaultconfig +import io.github.detekt.utils.YamlNode +import io.github.detekt.utils.keyValue +import io.github.detekt.utils.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.node internal fun YamlNode.printRuleSetPage(ruleSetPage: RuleSetPage) { printRuleSet(ruleSetPage.ruleSet, ruleSetPage.rules) diff --git a/detekt-generator/src/test/kotlin/io/gitlab/arturbosch/detekt/generator/printer/defaultconfig/RuleSetConfigPrinterTest.kt b/detekt-generator/src/test/kotlin/io/gitlab/arturbosch/detekt/generator/printer/defaultconfig/RuleSetConfigPrinterTest.kt index 35532232259..ee016a857d5 100644 --- a/detekt-generator/src/test/kotlin/io/gitlab/arturbosch/detekt/generator/printer/defaultconfig/RuleSetConfigPrinterTest.kt +++ b/detekt-generator/src/test/kotlin/io/gitlab/arturbosch/detekt/generator/printer/defaultconfig/RuleSetConfigPrinterTest.kt @@ -1,5 +1,6 @@ package io.gitlab.arturbosch.detekt.generator.printer.defaultconfig +import io.github.detekt.utils.yaml import io.gitlab.arturbosch.detekt.api.Config import io.gitlab.arturbosch.detekt.api.valuesWithReason import io.gitlab.arturbosch.detekt.generator.collection.Active @@ -8,7 +9,6 @@ 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 diff --git a/detekt-report-md/build.gradle.kts b/detekt-report-md/build.gradle.kts new file mode 100644 index 00000000000..1f7df0f6ae6 --- /dev/null +++ b/detekt-report-md/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("module") +} + +dependencies { + implementation(projects.detektMetrics) + implementation(projects.detektApi) + implementation(projects.detektUtils) + + testImplementation(testFixtures(projects.detektApi)) + testImplementation(libs.mockk) + testImplementation(libs.assertj) +} diff --git a/detekt-report-md/src/main/kotlin/io/github/detekt/report/md/MdOutputReport.kt b/detekt-report-md/src/main/kotlin/io/github/detekt/report/md/MdOutputReport.kt new file mode 100644 index 00000000000..45fed53e3c0 --- /dev/null +++ b/detekt-report-md/src/main/kotlin/io/github/detekt/report/md/MdOutputReport.kt @@ -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.utils.MarkdownContent +import io.github.detekt.utils.codeBlock +import io.github.detekt.utils.emptyLine +import io.github.detekt.utils.h1 +import io.github.detekt.utils.h2 +import io.github.detekt.utils.h3 +import io.github.detekt.utils.item +import io.github.detekt.utils.list +import io.github.detekt.utils.markdown +import io.github.detekt.utils.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 { + return ComplexityReportGenerator.create(detektion).generate().orEmpty() + } +} + +private fun MarkdownContent.renderMetrics(metrics: Collection) { + list { + metrics.forEach { item { "%,d ${it.type}".format(Locale.US, it.value) } } + } +} + +private fun MarkdownContent.renderComplexity(complexityReport: List) { + list { + complexityReport.forEach { item { it.trim() } } + } +} + +private fun MarkdownContent.renderGroup(group: String, findings: List) { + 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) { + 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>) { + 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, 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)" diff --git a/detekt-report-md/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.OutputReport b/detekt-report-md/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.OutputReport new file mode 100644 index 00000000000..83fe5d76a71 --- /dev/null +++ b/detekt-report-md/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.OutputReport @@ -0,0 +1 @@ +io.github.detekt.report.md.MdOutputReport diff --git a/detekt-report-md/src/test/kotlin/io/github/detekt/report/md/MdOutputReportSpec.kt b/detekt-report-md/src/test/kotlin/io/github/detekt/report/md/MdOutputReportSpec.kt new file mode 100644 index 00000000000..378d6643605 --- /dev/null +++ b/detekt-report-md/src/test/kotlin/io/github/detekt/report/md/MdOutputReportSpec.kt @@ -0,0 +1,224 @@ +package io.github.detekt.report.md + +import io.github.detekt.metrics.CognitiveComplexity +import io.github.detekt.metrics.processors.commentLinesKey +import io.github.detekt.metrics.processors.complexityKey +import io.github.detekt.metrics.processors.linesKey +import io.github.detekt.metrics.processors.logicalLinesKey +import io.github.detekt.metrics.processors.sourceLinesKey +import io.gitlab.arturbosch.detekt.api.Detektion +import io.gitlab.arturbosch.detekt.api.Finding +import io.gitlab.arturbosch.detekt.api.ProjectMetric +import io.gitlab.arturbosch.detekt.api.internal.whichDetekt +import io.gitlab.arturbosch.detekt.test.TestDetektion +import io.gitlab.arturbosch.detekt.test.createEntity +import io.gitlab.arturbosch.detekt.test.createFinding +import io.gitlab.arturbosch.detekt.test.createIssue +import io.mockk.every +import io.mockk.mockk +import org.assertj.core.api.Assertions.assertThat +import org.jetbrains.kotlin.com.intellij.psi.PsiFile +import org.jetbrains.kotlin.psi.KtElement +import org.junit.jupiter.api.Test + +class MdOutputReportSpec { + private val mdReport = MdOutputReport() + private val detektion = createTestDetektionWithMultipleSmells() + private val result = mdReport.render(detektion) + + @Test + fun `renders Markdown structure correctly`() { + assertThat(result).contains("Metrics") + assertThat(result).contains("Complexity Report") + assertThat(result).contains("Findings") + } + + @Test + fun `contains zero findings`() { + val result = mdReport.render(TestDetektion()) + + assertThat(result).contains("Findings (0)") + } + + @Test + fun `contains the total number of findings`() { + assertThat(result).contains("Findings (3)") + } + + @Test + fun `renders the 'generated with' text correctly`() { + val header = "generated with [detekt version ${whichDetekt()}](https://detekt.dev/) on " + + assertThat(result).contains(header) + } + + @Test + fun `renders the right file locations`() { + assertThat(result).contains("src/main/com/sample/Sample1.kt:9:17") + assertThat(result).contains("src/main/com/sample/Sample2.kt:13:17") + assertThat(result).contains("src/main/com/sample/Sample3.kt:14:16") + } + + @Test + fun `renders the right number of issues per rule`() { + assertThat(result).contains("id_a (2)") + assertThat(result).contains("id_b (1)") + } + + @Test + fun `renders the right violation messages for the rules`() { + assertThat(result).contains("Message finding 1") + assertThat(result).contains("Message finding 2") + } + + @Test + fun `renders the right violation description for the rules`() { + assertThat(result).contains("Description id_a") + assertThat(result).contains("Description id_b") + } + + @Test + fun `renders the right documentation links for the rules`() { + val detektion = object : TestDetektion() { + override val findings: Map> = mapOf( + "Style" to listOf( + createFinding(createIssue("ValCouldBeVar"), createEntity("")) + ), + "empty" to listOf( + createFinding(createIssue("EmptyBody"), createEntity("")), + createFinding(createIssue("EmptyIf"), createEntity("")) + ) + ) + } + + val result = mdReport.render(detektion) + assertThat(result).contains("[Documentation](https://detekt.dev/docs/rules/style#valcouldbevar)") + assertThat(result).contains("[Documentation](https://detekt.dev/docs/rules/empty#emptybody)") + assertThat(result).contains("[Documentation](https://detekt.dev/docs/rules/empty#emptyif)") + } + + @Test + fun `asserts that the generated HTML is the same even if we change the order of the findings`() { + val findings = findings() + val reversedFindings = findings + .reversedArray() + .map { (section, findings) -> section to findings.asReversed() } + .toTypedArray() + + val firstDetektion = createMdDetektion(*findings) + val secondDetektion = createMdDetektion(*reversedFindings) + + val firstReport = mdReport.render(firstDetektion) + val secondReport = mdReport.render(secondDetektion) + + assertThat(firstReport).isEqualTo(secondReport) + } +} + +private fun mockKtElement(): KtElement { + val ktElementMock = mockk() + val psiFileMock = mockk() + val code = """ + package com.example.test + + import io.github.* + + class Test() { + val greeting: String = "Hello, World!" + + init { + println(greetings) + } + + fun foo() { + println(greetings) + return this + } + } + """.trimIndent() + + every { psiFileMock.text } returns code + every { ktElementMock.containingFile } returns psiFileMock + return ktElementMock +} + +private fun createTestDetektionWithMultipleSmells(): Detektion { + val entity1 = createEntity( + path = "src/main/com/sample/Sample1.kt", + position = 9 to 17, + text = 17..20, + ktElement = mockKtElement(), + basePath = "/Users/tester/detekt/" + ) + val entity2 = createEntity( + path = "src/main/com/sample/Sample2.kt", + ktElement = mockKtElement(), + position = 13 to 17, + basePath = "/Users/tester/detekt/" + ) + val entity3 = createEntity( + path = "src/main/com/sample/Sample3.kt", + position = 14 to 16, + ktElement = mockKtElement(), + basePath = "/Users/tester/detekt/" + ) + + val issueA = createIssue("id_a") + val issueB = createIssue("id_b") + + return createMdDetektion( + "Section-1" to listOf( + createFinding(issueA, entity1, "Message finding 1"), + createFinding(issueA, entity2, "Message finding 2") + ), + "Section-2" to listOf( + createFinding(issueB, entity3, "") + ) + ).also { + it.addData(complexityKey, 10) + it.addData(CognitiveComplexity.KEY, 10) + it.addData(sourceLinesKey, 20) + it.addData(logicalLinesKey, 10) + it.addData(commentLinesKey, 2) + it.addData(linesKey, 2222) + } +} + +private fun createMdDetektion(vararg findingPairs: Pair>): Detektion { + return object : TestDetektion() { + override val findings: Map> = findingPairs.toMap() + + override val metrics: Collection = listOf( + ProjectMetric("M1", 10_000), + ProjectMetric("M2", 2) + ) + } +} + +private fun findings(): Array>> { + val issueA = createIssue("id_a") + val issueB = createIssue("id_b") + val issueC = createIssue("id_c") + + val entity1 = createEntity("src/main/com/sample/Sample1.kt", 11 to 5) + val entity2 = createEntity("src/main/com/sample/Sample1.kt", 22 to 2) + val entity3 = createEntity("src/main/com/sample/Sample1.kt", 11 to 2) + val entity4 = createEntity("src/main/com/sample/Sample2.kt", 1 to 1) + + return arrayOf( + "Section 1" to listOf( + createFinding(issueA, entity1), + createFinding(issueA, entity2), + createFinding(issueA, entity3), + createFinding(issueA, entity4), + createFinding(issueB, entity2), + createFinding(issueB, entity1), + createFinding(issueB, entity4) + ), + "Section 2" to listOf( + createFinding(issueB, entity3), + createFinding(issueC, entity1), + createFinding(issueC, entity2) + ) + ) +} diff --git a/detekt-utils/build.gradle.kts b/detekt-utils/build.gradle.kts index ed0de36f81c..1598ef9274c 100644 --- a/detekt-utils/build.gradle.kts +++ b/detekt-utils/build.gradle.kts @@ -1,3 +1,7 @@ plugins { id("module") } + +dependencies { + testImplementation(libs.assertj) +} diff --git a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/out/Markdown.kt b/detekt-utils/src/main/kotlin/io/github/detekt/utils/Markdown.kt similarity index 97% rename from detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/out/Markdown.kt rename to detekt-utils/src/main/kotlin/io/github/detekt/utils/Markdown.kt index 5989a3ecd93..a6879f4f6cf 100644 --- a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/out/Markdown.kt +++ b/detekt-utils/src/main/kotlin/io/github/detekt/utils/Markdown.kt @@ -1,6 +1,6 @@ @file:Suppress("detekt.TooManyFunctions") -package io.gitlab.arturbosch.detekt.generator.out +package io.github.detekt.utils sealed class Markdown(open var content: String = "") { fun append(value: String) { diff --git a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/out/Yaml.kt b/detekt-utils/src/main/kotlin/io/github/detekt/utils/Yaml.kt similarity index 98% rename from detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/out/Yaml.kt rename to detekt-utils/src/main/kotlin/io/github/detekt/utils/Yaml.kt index 100f1cbf7ba..92d3ab2c200 100644 --- a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/out/Yaml.kt +++ b/detekt-utils/src/main/kotlin/io/github/detekt/utils/Yaml.kt @@ -1,4 +1,4 @@ -package io.gitlab.arturbosch.detekt.generator.out +package io.github.detekt.utils sealed class YML(open val indent: Int = 0, open var content: String = "") { fun append(value: String) { diff --git a/detekt-generator/src/test/kotlin/io/gitlab/arturbosch/detekt/generator/out/YamlSpec.kt b/detekt-utils/src/test/kotlin/io/github/detekt/utils/YamlSpec.kt similarity index 99% rename from detekt-generator/src/test/kotlin/io/gitlab/arturbosch/detekt/generator/out/YamlSpec.kt rename to detekt-utils/src/test/kotlin/io/github/detekt/utils/YamlSpec.kt index 7dd481ba9a6..fcae48f37e3 100644 --- a/detekt-generator/src/test/kotlin/io/gitlab/arturbosch/detekt/generator/out/YamlSpec.kt +++ b/detekt-utils/src/test/kotlin/io/github/detekt/utils/YamlSpec.kt @@ -1,4 +1,4 @@ -package io.gitlab.arturbosch.detekt.generator.out +package io.github.detekt.utils import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Nested diff --git a/settings.gradle.kts b/settings.gradle.kts index f6743e77b55..b5d7e412ae3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,6 +17,7 @@ include("detekt-metrics") include("detekt-parser") include("detekt-psi-utils") include("detekt-report-html") +include("detekt-report-md") include("detekt-report-sarif") include("detekt-report-txt") include("detekt-report-xml")