<lambda>null1 #!/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 /**
20 * To run .kts files, follow these steps:
21 *
22 * 1. Download and install the Kotlin compiler (kotlinc). There are several ways to do this; see
23 * https://kotlinlang.org/docs/command-line.html
24 * 2. Run the script from the command line:
25 * <path_to>/kotlinc -script <script_file>.kts <arguments>
26 */
27
28 @file:Repository("https://repo1.maven.org/maven2")
29 @file:DependsOn("junit:junit:4.11")
30
31 import org.junit.Assert.assertEquals
32 import org.junit.Test
33 import org.w3c.dom.Document
34 import org.w3c.dom.Element
35 import org.w3c.dom.Node
36 import org.w3c.dom.NodeList
37 import org.xml.sax.InputSource
38 import java.io.File
39 import java.io.StringReader
40 import javax.xml.parsers.DocumentBuilderFactory
41 import kotlin.system.exitProcess
42
43 if (args.isEmpty()) {
44 println("Expected space-delimited list of files. Consider parsing all baselines with:")
45 println(" ./<path-to-script> `find . -name lint-baseline.xml`")
46 println("Also, consider updating baselines before running the script using:")
47 println(" ./gradlew updateLintBaseline --continue")
48 exitProcess(1)
49 }
50
51 if (args[0] == "test") {
52 runTests()
53 println("All tests passed")
54 exitProcess(0)
55 }
56
argnull57 val missingFiles = args.filter { arg -> !File(arg).exists() }
58 if (missingFiles.isNotEmpty()) {
59 println("Could not find files:\n ${missingFiles.joinToString("\n ")}")
60 exitProcess(1)
61 }
62
63 val executionPath = File(".")
64 // TODO: Consider adding argument "--output <output-file-path>"
65 val csvOutputFile = File("output.csv")
66 val csvData = StringBuilder()
67 val columnLabels = listOf(
68 "Baseline",
69 "ID",
70 "Message",
71 "Error",
72 "Location",
73 "Line"
74 )
75
76 // Emit labels into the CSV if it's being created from scratch.
77 if (!csvOutputFile.exists()) {
78 csvData.append(columnLabels.joinToString(","))
79 csvData.append("\n")
80 }
81
82 // For each file, emit one issue per line into the CSV.
lintBaselinePathnull83 args.forEach { lintBaselinePath ->
84 val lintBaselineFile = File(lintBaselinePath)
85 println("Parsing ${lintBaselineFile.path}...")
86
87 val lintIssuesList = LintBaselineParser.parse(lintBaselineFile)
88 lintIssuesList.forEach { lintIssues ->
89 lintIssues.issues.forEach { lintIssue ->
90 val columns = listOf(
91 lintIssues.file.toRelativeString(executionPath),
92 lintIssue.id,
93 lintIssue.message,
94 lintIssue.errorLines.joinToString("\n"),
95 lintIssue.locations.getOrNull(0)?.file ?: "",
96 lintIssue.locations.getOrNull(0)?.line?.toString() ?: "",
97 )
98 csvData.append(columns.joinToString(",") { data ->
99 // Wrap every item with quotes and escape existing quotes.
100 "\"${data.replace("\"", "\"\"")}\""
101 })
102 csvData.append("\n")
103 }
104 }
105 }
106
107 csvOutputFile.appendText(csvData.toString())
108
109 println("Wrote CSV output to ${csvOutputFile.path} for ${args.size} baselines")
110
111 object LintBaselineParser {
parsenull112 fun parse(lintBaselineFile: File): List<LintIssues> {
113 val builderFactory = DocumentBuilderFactory.newInstance()!!
114 val docBuilder = builderFactory.newDocumentBuilder()!!
115 val doc: Document = docBuilder.parse(lintBaselineFile)
116 return parseIssuesListFromDocument(doc, lintBaselineFile)
117 }
118
parsenull119 fun parse(lintBaselineText: String): List<LintIssues> {
120 val builderFactory = DocumentBuilderFactory.newInstance()!!
121 val docBuilder = builderFactory.newDocumentBuilder()!!
122 val doc: Document = docBuilder.parse(InputSource(StringReader(lintBaselineText)))
123 return parseIssuesListFromDocument(doc, File("."))
124 }
125
parseIssuesListFromDocumentnull126 private fun parseIssuesListFromDocument(doc: Document, file: File): List<LintIssues> =
127 doc.getElementsByTagName("issues").mapElementsNotNull { issues ->
128 LintIssues(
129 file = file,
130 issues = parseIssueListFromIssues(issues),
131 )
132 }
133
parseIssueListFromIssuesnull134 private fun parseIssueListFromIssues(issues: Element): List<LintIssue> =
135 issues.getElementsByTagName("issue").mapElementsNotNull { issue ->
136 LintIssue(
137 id = issue.getAttribute("id"),
138 message = issue.getAttribute("message"),
139 errorLines = parseErrorLineListFromIssue(issue),
140 locations = parseLocationListFromIssue(issue),
141 )
142 }
143
parseLocationListFromIssuenull144 private fun parseLocationListFromIssue(issue: Element): List<LintLocation> =
145 issue.getElementsByTagName("location").mapElementsNotNull { location ->
146 LintLocation(
147 file = location.getAttribute("file"),
148 line = location.getAttribute("line")?.toIntOrNull() ?: 0,
149 column = location.getAttribute("column")?.toIntOrNull() ?: 0,
150 )
151 }
152
parseErrorLineListFromIssuenull153 private fun parseErrorLineListFromIssue(issue: Element): List<String> {
154 val list = mutableListOf<String>()
155 var i = 1
156 while (issue.hasAttribute("errorLine$i")) {
157 issue.getAttribute("errorLine$i")?.let{ list.add(it) }
158 i++
159 }
160 return list.toList()
161 }
162
163 // This MUST be inside the class, otherwise we'll get a compilation error.
mapElementsNotNullnull164 private fun <T> NodeList.mapElementsNotNull(transform: (element: Element) -> T?): List<T> {
165 val list = mutableListOf<T>()
166 for (i in 0 until length) {
167 val node = item(i)
168 if (node.nodeType == Node.ELEMENT_NODE && node is Element) {
169 transform(node)?.let { list.add(it) }
170 }
171 }
172 return list.toList()
173 }
174 }
175
176 data class LintIssues(
177 val file: File,
178 val issues: List<LintIssue>,
179 )
180
181 data class LintIssue(
182 val id: String,
183 val message: String,
184 val errorLines: List<String>,
185 val locations: List<LintLocation>,
186 )
187
188 data class LintLocation(
189 val file: String,
190 val line: Int,
191 val column: Int,
192 )
193
runTestsnull194 fun runTests() {
195 `Baseline with one issue parses contents correctly`()
196 `Empty baseline has no issues`()
197 }
198
199 @Test
Baseline with one issue parses contents correctlynull200 fun `Baseline with one issue parses contents correctly`() {
201 var lintBaselineText = """
202 <?xml version="1.0" encoding="UTF-8"?>
203 <issues format="5" by="lint 4.2.0-beta06" client="gradle" variant="debug" version="4.2.0-beta06">
204 <issue
205 id="ClassVerificationFailure"
206 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."
207 errorLine1=" PrintAttributes attr = new PrintAttributes.Builder()"
208 errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
209 <location
210 file="src/main/java/androidx/print/PrintHelper.java"
211 line="271"
212 column="32"/>
213 </issue>
214 </issues>
215 """.trimIndent()
216
217 var listIssues = LintBaselineParser.parse(lintBaselineText)
218 assertEquals(1, listIssues.size)
219
220 var issues = listIssues[0].issues
221 assertEquals(1, issues.size)
222
223 var issue = issues[0]
224 assertEquals("ClassVerificationFailure", issue.id)
225 assertEquals("This call references a method added in API level 19; however, the containing " +
226 "class androidx.print.PrintHelper is reachable from earlier API levels and will fail " +
227 "run-time class verification.", issue.message)
228 assertEquals(2, issue.errorLines.size)
229 assertEquals(1, issue.locations.size)
230
231 var location = issue.locations[0]
232 assertEquals("src/main/java/androidx/print/PrintHelper.java", location.file)
233 assertEquals(271, location.line)
234 assertEquals(32, location.column)
235 }
236
237 @Test
Empty baseline has no issuesnull238 fun `Empty baseline has no issues`() {
239 var lintBaselineText = """
240 <?xml version="1.0" encoding="UTF-8"?>
241 <issues format="5" by="lint 4.2.0-beta06" client="gradle" version="4.2.0-beta06">
242
243 </issues>
244 """.trimIndent()
245
246 var listIssues = LintBaselineParser.parse(lintBaselineText)
247 assertEquals(1, listIssues.size)
248
249 var issues = listIssues[0].issues
250 assertEquals(0, issues.size)
251 }
252