-
-
Notifications
You must be signed in to change notification settings - Fork 757
/
XmlReportMerger.kt
149 lines (131 loc) · 5.56 KB
/
XmlReportMerger.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
package io.gitlab.arturbosch.detekt.report
import org.w3c.dom.Document
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import java.io.File
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
/**
* A naive implementation to merge xml assuming all input xml are written by detekt.
*/
object XmlReportMerger {
private val documentBuilder by lazy { DocumentBuilderFactory.newInstance().newDocumentBuilder() }
fun merge(reportFiles: Collection<File>, output: File) {
val distinctErrorsBySourceFile = DetektCheckstyleReports(reportFiles)
.parseCheckstyleToSourceFileNodes()
.distinctErrorsGroupedBySourceFile()
val mergedCheckstyle = createMergedCheckstyle(distinctErrorsBySourceFile)
TransformerFactory.newInstance().newTransformer().run {
setOutputProperty(OutputKeys.INDENT, "yes")
setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2")
transform(DOMSource(mergedCheckstyle), StreamResult(output.writer()))
}
}
private fun createMergedCheckstyle(distinctErrorsBySourceFile: Map<String, List<Node>>): Document {
val mergedDocument = documentBuilder.newDocument().apply {
xmlStandalone = true
}
val mergedCheckstyleNode = mergedDocument.createElement("checkstyle")
mergedCheckstyleNode.setAttribute("version", "4.3")
mergedDocument.appendChild(mergedCheckstyleNode)
distinctErrorsBySourceFile.forEach { (fileName, errorNodes) ->
mergedCheckstyleNode.appendChild(
mergedDocument.createElement("file").apply {
setAttribute("name", fileName)
errorNodes.forEach {
appendChild(mergedDocument.importNode(it, true))
}
}
)
}
return mergedDocument
}
/** A list of checkstyle xml files written by Detekt */
private class DetektCheckstyleReports(private val files: Collection<File>) {
/**
* Parses a list of `file` nodes matching the following topology
*
* ```xml
* <checkstyle>
* <file/>
* </checkstyle>
* ```
*
* @see CheckstyleSourceFileNodes
*/
fun parseCheckstyleToSourceFileNodes() =
CheckstyleSourceFileNodes(
files.filter { reportFile -> reportFile.exists() }
.flatMap { existingReportFile ->
val checkstyleNode = documentBuilder.parse(existingReportFile.inputStream())
val sourceFileNodes = checkstyleNode.documentElement.childNodes.asSequence().filterWhitespace()
sourceFileNodes
}
)
}
/**
* A list of checkstyle `file` nodes that may contain 0 to many `error` nodes
*
* ```xml
* <file>
* <error>
* </file>
* ```
*/
private class CheckstyleSourceFileNodes(private val nodes: List<Node>) {
/** Returns a map containing only distinct error nodes, grouped by file name */
fun distinctErrorsGroupedBySourceFile() = nodes
.flatMap { fileNode ->
val fileNameAttribute = fileNode.attributes.getNamedItem("name").nodeValue
val errorNodes = fileNode.childNodes.asSequence().filterWhitespace()
errorNodes.map { errorNode ->
CheckstyleErrorNodeWithFileData(
errorID = errorID(fileNameAttribute, errorNode),
fileName = fileNameAttribute,
errorNode = errorNode
)
}
}
.distinctBy { it.errorID }
.groupBy({ it.fileName }, { it.errorNode })
private fun errorID(fileNameAttribute: String, errorNode: Node): Any {
// error nodes are expected to take the form of at least <error line="#" column="#" source="ruleName"/>
val line = errorNode.attributes.getNamedItem("line")?.nodeValue
val column = errorNode.attributes.getNamedItem("column")?.nodeValue
val source = errorNode.attributes.getNamedItem("source")?.nodeValue
return if (line != null && column != null && source != null) {
// data class provides convenient hashCode/equals based on these attributes
ErrorID(fileName = fileNameAttribute, line = line, column = column, source = source)
} else {
// if the error node does not contain the expected attributes,
// use org.w3c.dom.Node's more strict hashCode/equals method to determine error uniqueness
errorNode
}
}
private class CheckstyleErrorNodeWithFileData(
val errorID: Any,
val fileName: String,
val errorNode: Node
)
private data class ErrorID(
val fileName: String,
val line: String,
val column: String,
val source: String
)
}
/**
* Use code instead of XSLT to exclude whitespaces.
*/
private fun Sequence<Node>.filterWhitespace(): Sequence<Node> = asSequence().filterNot {
it.nodeType == Node.TEXT_NODE && it.textContent.isBlank()
}
private fun NodeList.asSequence() = sequence {
for (index in 0 until length) {
yield(item(index))
}
}
}