-
-
Notifications
You must be signed in to change notification settings - Fork 757
/
HtmlOutputReport.kt
197 lines (169 loc) · 6.92 KB
/
HtmlOutputReport.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
package io.github.detekt.report.html
import io.github.detekt.metrics.ComplexityReportGenerator
import io.github.detekt.utils.openSafeStream
import io.gitlab.arturbosch.detekt.api.Detektion
import io.gitlab.arturbosch.detekt.api.Issue
import io.gitlab.arturbosch.detekt.api.OutputReport
import io.gitlab.arturbosch.detekt.api.ProjectMetric
import io.gitlab.arturbosch.detekt.api.RuleSet
import io.gitlab.arturbosch.detekt.api.SetupContext
import io.gitlab.arturbosch.detekt.api.TextLocation
import io.gitlab.arturbosch.detekt.api.getOrNull
import io.gitlab.arturbosch.detekt.api.internal.BuiltInOutputReport
import io.gitlab.arturbosch.detekt.api.internal.whichDetekt
import kotlinx.html.CommonAttributeGroupFacadeFlowInteractiveContent
import kotlinx.html.FlowContent
import kotlinx.html.FlowOrInteractiveContent
import kotlinx.html.HTMLTag
import kotlinx.html.HtmlTagMarker
import kotlinx.html.TagConsumer
import kotlinx.html.a
import kotlinx.html.attributesMapOf
import kotlinx.html.details
import kotlinx.html.div
import kotlinx.html.h3
import kotlinx.html.id
import kotlinx.html.li
import kotlinx.html.span
import kotlinx.html.stream.createHTML
import kotlinx.html.ul
import kotlinx.html.visit
import java.nio.file.Path
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Locale
import kotlin.io.path.absolute
import kotlin.io.path.invariantSeparatorsPathString
import kotlin.io.path.relativeTo
private const val DEFAULT_TEMPLATE = "default-html-report-template.html"
private const val PLACEHOLDER_METRICS = "@@@metrics@@@"
private const val PLACEHOLDER_ISSUES = "@@@issues@@@"
private const val PLACEHOLDER_COMPLEXITY_REPORT = "@@@complexity@@@"
private const val PLACEHOLDER_VERSION = "@@@version@@@"
private const val PLACEHOLDER_DATE = "@@@date@@@"
private const val DETEKT_WEBSITE_BASE_URL = "https://detekt.dev"
/**
* Contains rule violations and metrics formatted in a human friendly way, so that it can be inspected in a web browser.
* See: https://detekt.dev/configurations.html#output-reports
*/
class HtmlOutputReport : BuiltInOutputReport, OutputReport() {
override val id: String = "HtmlOutputReport"
override val ending = "html"
var basePath: Path? = null
override fun init(context: SetupContext) {
basePath = context.getOrNull<Path>(DETEKT_OUTPUT_REPORT_BASE_PATH_KEY)?.absolute()
}
override fun render(detektion: Detektion) =
javaClass.getResource("/$DEFAULT_TEMPLATE")!!
.openSafeStream()
.bufferedReader()
.use { it.readText() }
.replace(PLACEHOLDER_VERSION, renderVersion())
.replace(PLACEHOLDER_DATE, renderDate())
.replace(PLACEHOLDER_METRICS, renderMetrics(detektion.metrics))
.replace(PLACEHOLDER_COMPLEXITY_REPORT, renderComplexity(getComplexityMetrics(detektion)))
.replace(PLACEHOLDER_ISSUES, renderIssues(detektion.issues))
private fun renderVersion(): String = whichDetekt()
private fun renderDate(): String {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
return "${OffsetDateTime.now(ZoneOffset.UTC).format(formatter)} UTC"
}
private fun renderMetrics(metrics: Collection<ProjectMetric>) = createHTML().div {
ul {
metrics.forEach {
li { text("%,d ${it.type}".format(Locale.ROOT, it.value)) }
}
}
}
private fun renderComplexity(complexityReport: List<String>) = createHTML().div {
ul {
complexityReport.forEach {
li { text(it.trim()) }
}
}
}
private fun renderIssues(issues: List<Issue>) = createHTML().div {
val total = issues.count()
text("Total: %,d".format(Locale.ROOT, total))
issues
.groupBy { it.ruleInfo.ruleSetId }
.toList()
.sortedBy { (group, _) -> group.value }
.forEach { (group, groupIssues) ->
renderGroup(group, groupIssues)
}
}
private fun FlowContent.renderGroup(group: RuleSet.Id, issues: List<Issue>) {
h3 { text("$group: %,d".format(Locale.ROOT, issues.size)) }
issues
.groupBy { it.ruleInfo }
.toList()
.sortedBy { (ruleInfo, _) -> ruleInfo.id.value }
.forEach { (ruleInfo, ruleIssues) ->
renderRule(ruleInfo, ruleIssues)
}
}
private fun FlowContent.renderRule(ruleInfo: Issue.RuleInfo, issues: List<Issue>) {
val ruleId = ruleInfo.id.value
val ruleSetId = ruleInfo.ruleSetId.value
details {
id = ruleId
open = true
summary("rule-container") {
span("rule") { text("$ruleId: %,d ".format(Locale.ROOT, issues.size)) }
span("description") { text(ruleInfo.description) }
}
a("$DETEKT_WEBSITE_BASE_URL/docs/rules/${ruleSetId.lowercase()}#${ruleId.lowercase()}") {
+"Documentation"
}
ul {
issues
.sortedWith(
compareBy(
{ it.location.filePath.absolutePath.toString() },
{ it.location.source.line },
{ it.location.source.column },
)
)
.forEach {
li {
renderIssue(it)
}
}
}
}
}
private fun FlowContent.renderIssue(issue: Issue) {
val filePath = basePath?.let { issue.location.filePath.absolutePath.relativeTo(it) }
?: issue.location.filePath.absolutePath
val pathString = filePath.invariantSeparatorsPathString
span("location") {
text(
"$pathString:${issue.location.source.line}:${issue.location.source.column}"
)
}
if (issue.message.isNotEmpty()) {
span("message") { text(issue.message) }
}
val psiFile = issue.entity.ktElement?.containingFile
if (psiFile != null) {
val lineSequence = psiFile.text.splitToSequence('\n')
snippetCode(issue.ruleInfo.id, lineSequence, issue.location.source, issue.location.text.length())
}
}
private fun getComplexityMetrics(detektion: Detektion): List<String> {
return ComplexityReportGenerator.create(detektion).generate().orEmpty()
}
}
@HtmlTagMarker
private fun FlowOrInteractiveContent.summary(
classes: String,
block: SUMMARY.() -> Unit = {}
): Unit = SUMMARY(attributesMapOf("class", classes), consumer).visit(block)
private class SUMMARY(
initialAttributes: Map<String, String>,
override val consumer: TagConsumer<*>
) : HTMLTag("summary", consumer, initialAttributes, null, false, false),
CommonAttributeGroupFacadeFlowInteractiveContent
private fun TextLocation.length(): Int = end - start