blob: 82606f26f9249b70e57b11678fb65d13899ae628 [file] [log] [blame]
Alan Viverette15959fb2021-05-18 12:00:23 -04001#!/usr/bin/env kotlin
2
3/*
4 * Copyright 2021 The Android Open Source Project
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19@file:Repository("https://repo1.maven.org/maven2")
20@file:DependsOn("junit:junit:4.11")
21
22import org.junit.Assert.assertEquals
23import org.junit.Test
24import org.w3c.dom.Document
25import org.w3c.dom.Element
26import org.w3c.dom.Node
27import org.w3c.dom.NodeList
28import org.xml.sax.InputSource
29import java.io.File
30import java.io.StringReader
31import javax.xml.parsers.DocumentBuilderFactory
32import kotlin.system.exitProcess
33
34if (args[0] == "test") {
35 runTests()
36 println("All tests passed")
37 exitProcess(0)
38}
39
40if (args.isEmpty()) {
41 println("Expected space-delimited list of files. Consider parsing all baselines with:")
42 println(" ./<path-to-script> `find . -name lint-baseline.xml`")
43 println("Also, consider updating baselines before running the script using:")
44 println(" ./gradlew lintDebug -PupdateLintBaseline --continue")
45 exitProcess(1)
46}
47
48val missingFiles = args.filter { arg -> !File(arg).exists() }
49if (missingFiles.isNotEmpty()) {
50 println("Could not find files:\n ${missingFiles.joinToString("\n ")}")
51 exitProcess(1)
52}
53
54val executionPath = File(".")
55// TODO: Consider adding argument "--output <output-file-path>"
56val csvOutputFile = File("output.csv")
57val csvData = StringBuilder()
58val columnLabels = listOf(
59 "Baseline",
60 "ID",
61 "Message",
62 "Error",
63 "Location",
64 "Line"
65)
66
67// Emit labels into the CSV if it's being created from scratch.
68if (!csvOutputFile.exists()) {
69 csvData.append(columnLabels.joinToString(","))
70 csvData.append("\n")
71}
72
73// For each file, emit one issue per line into the CSV.
74args.forEach { lintBaselinePath ->
75 val lintBaselineFile = File(lintBaselinePath)
76 println("Parsing ${lintBaselineFile.path}...")
77
78 val lintIssuesList = LintBaselineParser.parse(lintBaselineFile)
79 lintIssuesList.forEach { lintIssues ->
80 lintIssues.issues.forEach { lintIssue ->
81 val columns = listOf(
82 lintIssues.file.toRelativeString(executionPath),
83 lintIssue.id,
84 lintIssue.message,
85 lintIssue.errorLines.joinToString("\n"),
86 lintIssue.locations.getOrNull(0)?.file ?: "",
87 lintIssue.locations.getOrNull(0)?.line?.toString() ?: "",
88 )
89 csvData.append(columns.joinToString(",") { data ->
90 // Wrap every item with quotes and escape existing quotes.
91 "\"${data.replace("\"", "\"\"")}\""
92 })
93 csvData.append("\n")
94 }
95 }
96}
97
98csvOutputFile.appendText(csvData.toString())
99
100println("Wrote CSV output to ${csvOutputFile.path} for ${args.size} baselines")
101
102object LintBaselineParser {
103 fun parse(lintBaselineFile: File): List<LintIssues> {
104 val builderFactory = DocumentBuilderFactory.newInstance()!!
105 val docBuilder = builderFactory.newDocumentBuilder()!!
106 val doc: Document = docBuilder.parse(lintBaselineFile)
107 return parseIssuesListFromDocument(doc, lintBaselineFile)
108 }
109
110 fun parse(lintBaselineText: String): List<LintIssues> {
111 val builderFactory = DocumentBuilderFactory.newInstance()!!
112 val docBuilder = builderFactory.newDocumentBuilder()!!
113 val doc: Document = docBuilder.parse(InputSource(StringReader(lintBaselineText)))
114 return parseIssuesListFromDocument(doc, File("."))
115 }
116
117 private fun parseIssuesListFromDocument(doc: Document, file: File): List<LintIssues> =
118 doc.getElementsByTagName("issues").mapElementsNotNull { issues ->
119 LintIssues(
120 file = file,
121 issues = parseIssueListFromIssues(issues),
122 )
123 }
124
125 private fun parseIssueListFromIssues(issues: Element): List<LintIssue> =
126 issues.getElementsByTagName("issue").mapElementsNotNull { issue ->
127 LintIssue(
128 id = issue.getAttribute("id"),
129 message = issue.getAttribute("message"),
130 errorLines = parseErrorLineListFromIssue(issue),
131 locations = parseLocationListFromIssue(issue),
132 )
133 }
134
135 private fun parseLocationListFromIssue(issue: Element): List<LintLocation> =
136 issue.getElementsByTagName("location").mapElementsNotNull { location ->
137 LintLocation(
138 file = location.getAttribute("file"),
139 line = location.getAttribute("line")?.toIntOrNull() ?: 0,
140 column = location.getAttribute("column")?.toIntOrNull() ?: 0,
141 )
142 }
143
144 private fun parseErrorLineListFromIssue(issue: Element): List<String> {
145 val list = mutableListOf<String>()
146 var i = 1
147 while (issue.hasAttribute("errorLine$i")) {
148 issue.getAttribute("errorLine$i")?.let{ list.add(it) }
149 i++
150 }
151 return list.toList()
152 }
153
154 // This MUST be inside the class, otherwise we'll get a compilation error.
155 private fun <T> NodeList.mapElementsNotNull(transform: (element: Element) -> T?): List<T> {
156 val list = mutableListOf<T>()
157 for (i in 0 until length) {
158 val node = item(i)
159 if (node.nodeType == Node.ELEMENT_NODE && node is Element) {
160 transform(node)?.let { list.add(it) }
161 }
162 }
163 return list.toList()
164 }
165}
166
167data class LintIssues(
168 val file: File,
169 val issues: List<LintIssue>,
170)
171
172data class LintIssue(
173 val id: String,
174 val message: String,
175 val errorLines: List<String>,
176 val locations: List<LintLocation>,
177)
178
179data class LintLocation(
180 val file: String,
181 val line: Int,
182 val column: Int,
183)
184
185fun runTests() {
186 `Baseline with one issue parses contents correctly`()
187 `Empty baseline has no issues`()
188}
189
190@Test
191fun `Baseline with one issue parses contents correctly`() {
192 /* ktlint-disable max-line-length */
193 var lintBaselineText = """
194 <?xml version="1.0" encoding="UTF-8"?>
195 <issues format="5" by="lint 4.2.0-beta06" client="gradle" variant="debug" version="4.2.0-beta06">
196
197 <issue
198 id="ClassVerificationFailure"
199 message="This call references a method added in API level 19; however, the containing class androidx.print.PrintHelper is reachable from earlier API levels and will fail run-time class verification."
200 errorLine1=" PrintAttributes attr = new PrintAttributes.Builder()"
201 errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
202 <location
203 file="src/main/java/androidx/print/PrintHelper.java"
204 line="271"
205 column="32"/>
206 </issue>
207 </issues>
208 """.trimIndent()
209 /* ktlint-enable max-line-length */
210
211 var listIssues = LintBaselineParser.parse(lintBaselineText)
212 assertEquals(1, listIssues.size)
213
214 var issues = listIssues[0].issues
215 assertEquals(1, issues.size)
216
217 var issue = issues[0]
218 assertEquals("ClassVerificationFailure", issue.id)
219 assertEquals("This call references a method added in API level 19; however, the containing " +
220 "class androidx.print.PrintHelper is reachable from earlier API levels and will fail " +
221 "run-time class verification.", issue.message)
222 assertEquals(2, issue.errorLines.size)
223 assertEquals(1, issue.locations.size)
224
225 var location = issue.locations[0]
226 assertEquals("src/main/java/androidx/print/PrintHelper.java", location.file)
227 assertEquals(271, location.line)
228 assertEquals(32, location.column)
229}
230
231@Test
232fun `Empty baseline has no issues`() {
233 /* ktlint-disable max-line-length */
234 var lintBaselineText = """
235 <?xml version="1.0" encoding="UTF-8"?>
236 <issues format="5" by="lint 4.2.0-beta06" client="gradle" version="4.2.0-beta06">
237
238 </issues>
239 """.trimIndent()
240 /* ktlint-enable max-line-length */
241
242 var listIssues = LintBaselineParser.parse(lintBaselineText)
243 assertEquals(1, listIssues.size)
244
245 var issues = listIssues[0].issues
246 assertEquals(0, issues.size)
247}