<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