1 /*
<lambda>null2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tools.metalava
18 
19 import com.android.tools.metalava.doclava1.Issues
20 import com.android.tools.metalava.model.ClassItem
21 import com.android.tools.metalava.model.FieldItem
22 import com.android.tools.metalava.model.Item
23 import com.android.tools.metalava.model.MethodItem
24 import com.android.tools.metalava.model.PackageItem
25 import com.android.tools.metalava.model.ParameterItem
26 import com.android.tools.metalava.model.configuration
27 import com.intellij.openapi.vfs.VfsUtilCore
28 import com.intellij.psi.PsiClass
29 import com.intellij.psi.PsiElement
30 import com.intellij.psi.PsiField
31 import com.intellij.psi.PsiFile
32 import com.intellij.psi.PsiMethod
33 import com.intellij.psi.PsiPackage
34 import com.intellij.psi.PsiParameter
35 import org.jetbrains.kotlin.psi.KtClass
36 import org.jetbrains.kotlin.psi.KtProperty
37 import org.jetbrains.kotlin.psi.psiUtil.containingClass
38 import org.jetbrains.kotlin.psi.psiUtil.parameterIndex
39 import java.io.File
40 import java.io.PrintWriter
41 import kotlin.text.Charsets.UTF_8
42 
43 const val DEFAULT_BASELINE_NAME = "baseline.txt"
44 
45 class Baseline(
46     /** Description of this baseline. e.g. "api-lint. */
47     val description: String,
48     val file: File?,
49     var updateFile: File?,
50     var merge: Boolean = false,
51     private var headerComment: String = "",
52     /**
53      * Whether, when updating the baseline, we allow the metalava run to pass even if the baseline
54      * does not contain all issues that would normally fail the run (by default ERROR level).
55      */
56     var silentUpdate: Boolean = updateFile != null && updateFile.path == file?.path,
57     private var format: FileFormat = FileFormat.BASELINE
58 ) {
59 
60     /** Map from issue id to element id to message */
61     private val map = HashMap<Issues.Issue, MutableMap<String, String>>()
62 
63     init {
64         if (file?.isFile == true && (!silentUpdate || merge)) {
65             // We've set a baseline for a nonexistent file: read it
66             read()
67         }
68     }
69 
70     /** Returns true if the given issue is listed in the baseline, otherwise false */
71     fun mark(element: Item, message: String, issue: Issues.Issue): Boolean {
72         val elementId = getBaselineKey(element)
73         return mark(elementId, message, issue)
74     }
75 
76     /** Returns true if the given issue is listed in the baseline, otherwise false */
77     fun mark(element: PsiElement, message: String, issue: Issues.Issue): Boolean {
78         val elementId = getBaselineKey(element)
79         return mark(elementId, message, issue)
80     }
81 
82     /** Returns true if the given issue is listed in the baseline, otherwise false */
83     fun mark(file: File, message: String, issue: Issues.Issue): Boolean {
84         val elementId = getBaselineKey(file)
85         return mark(elementId, message, issue)
86     }
87 
88     private fun mark(elementId: String, @Suppress("UNUSED_PARAMETER") message: String, issue: Issues.Issue): Boolean {
89         val idMap: MutableMap<String, String>? = map[issue] ?: run {
90             if (updateFile != null) {
91                 if (options.baselineErrorsOnly && configuration.getSeverity(issue) != Severity.ERROR) {
92                     return true
93                 }
94                 val new = HashMap<String, String>()
95                 map[issue] = new
96                 new
97             } else {
98                 null
99             }
100         }
101 
102         val oldMessage: String? = idMap?.get(elementId)
103         if (oldMessage != null) {
104             // for now not matching messages; the id's are unique enough and allows us
105             // to tweak issue messages compatibly without recording all the deltas here
106             return true
107         }
108 
109         if (updateFile != null) {
110             idMap?.set(elementId, message)
111 
112             // When creating baselines don't report issues
113             if (silentUpdate) {
114                 return true
115             }
116         }
117 
118         return false
119     }
120 
121     private fun getBaselineKey(element: Item): String {
122         return when (element) {
123             is ClassItem -> element.qualifiedName()
124             is MethodItem -> element.containingClass().qualifiedName() + "#" +
125                 element.name() + "(" + element.parameters().joinToString { it.type().toSimpleType() } + ")"
126             is FieldItem -> element.containingClass().qualifiedName() + "#" + element.name()
127             is PackageItem -> element.qualifiedName()
128             is ParameterItem -> getBaselineKey(element.containingMethod()) + " parameter #" + element.parameterIndex
129             else -> element.describe(false)
130         }
131     }
132 
133     private fun getBaselineKey(element: PsiElement): String {
134         return when (element) {
135             is PsiClass -> element.qualifiedName ?: element.name ?: "?"
136             is KtClass -> element.fqName?.asString() ?: element.name ?: "?"
137             is PsiMethod -> {
138                 val containingClass = element.containingClass
139                 val name = element.name
140                 val parameterList = "(" + element.parameterList.parameters.joinToString { it.type.canonicalText } + ")"
141                 if (containingClass != null) {
142                     getBaselineKey(containingClass) + "#" + name + parameterList
143                 } else {
144                     name + parameterList
145                 }
146             }
147             is PsiField -> {
148                 val containingClass = element.containingClass
149                 val name = element.name
150                 if (containingClass != null) {
151                     getBaselineKey(containingClass) + "#" + name
152                 } else {
153                     name
154                 }
155             }
156             is KtProperty -> {
157                 val containingClass = element.containingClass()
158                 val name = element.nameAsSafeName.asString()
159                 if (containingClass != null) {
160                     getBaselineKey(containingClass) + "#" + name
161                 } else {
162                     name
163                 }
164             }
165             is PsiPackage -> element.qualifiedName
166             is PsiParameter -> {
167                 val method = element.declarationScope.parent
168                 if (method is PsiMethod) {
169                     getBaselineKey(method) + " parameter #" + element.parameterIndex()
170                 } else {
171                     "?"
172                 }
173             }
174             is PsiFile -> {
175                 val virtualFile = element.virtualFile
176                 val file = VfsUtilCore.virtualToIoFile(virtualFile)
177                 return getBaselineKey(file)
178             }
179             else -> element.toString()
180         }
181     }
182 
183     private fun getBaselineKey(file: File): String {
184         val path = file.path
185         for (sourcePath in options.sourcePath) {
186             if (path.startsWith(sourcePath.path)) {
187                 return path.substring(sourcePath.path.length).replace('\\', '/').removePrefix("/")
188             }
189         }
190 
191         return path.replace('\\', '/')
192     }
193 
194     /** Close the baseline file. If "update file" is set, update this file, and returns TRUE. If not, returns false. */
195     fun close(): Boolean {
196         return write()
197     }
198 
199     private fun read() {
200         val file = this.file ?: return
201         val lines = file.readLines(UTF_8)
202         for (i in 0 until lines.size - 1) {
203             val line = lines[i]
204             if (line.startsWith("//") ||
205                 line.startsWith("#") ||
206                 line.isBlank() ||
207                 line.startsWith(" ")) {
208                 continue
209             }
210             val idEnd = line.indexOf(':')
211             val elementEnd = line.indexOf(':', idEnd + 1)
212             if (idEnd == -1 || elementEnd == -1) {
213                 println("Invalid metalava baseline format: $line")
214             }
215             val issueId = line.substring(0, idEnd).trim()
216             val elementId = line.substring(idEnd + 2, elementEnd).trim()
217 
218             // Unless merging, we don't need the actual messages since we're only matching by
219             // issue id and API location, so don't bother computing.
220             val message = if (merge) lines[i + 1].trim() else ""
221 
222             val issue = Issues.findIssueById(issueId)
223             if (issue == null) {
224                 println("Invalid metalava baseline file: unknown issue id '$issueId'")
225             } else {
226                 val newIdMap = map[issue] ?: run {
227                     val new = HashMap<String, String>()
228                     map[issue] = new
229                     new
230                 }
231                 newIdMap[elementId] = message
232             }
233         }
234     }
235 
236     private fun write(): Boolean {
237         val updateFile = this.updateFile ?: return false
238         if (map.isNotEmpty() || !options.deleteEmptyBaselines) {
239             val sb = StringBuilder()
240             sb.append(format.header())
241             sb.append(headerComment)
242 
243             map.keys.asSequence().sortedBy { it.name }.forEach { issue ->
244                 val idMap = map[issue]
245                 idMap?.keys?.sorted()?.forEach { elementId ->
246                     val message = idMap[elementId]!!
247                     sb.append(issue.name).append(": ")
248                     sb.append(elementId)
249                     sb.append(":\n    ")
250                     sb.append(message).append('\n')
251                 }
252                 sb.append("\n\n")
253             }
254 
255             if (sb.endsWith("\n\n")) {
256                 sb.setLength(sb.length - 2)
257             }
258 
259             updateFile.parentFile?.mkdirs()
260             updateFile.writeText(sb.toString(), UTF_8)
261         } else {
262             updateFile.delete()
263         }
264         return true
265     }
266 
267     fun dumpStats(writer: PrintWriter) {
268         val counts = mutableMapOf<Issues.Issue, Int>()
269         map.keys.asSequence().forEach { issue ->
270             val idMap = map[issue]
271             val count = idMap?.count() ?: 0
272             counts[issue] = count
273         }
274 
275         writer.println("Baseline issue type counts for $description baseline:")
276         writer.println("" +
277             "    Count Issue Id                       Severity\n" +
278             "    ---------------------------------------------\n")
279         val list = counts.entries.toMutableList()
280         list.sortWith(compareBy({ -it.value }, { it.key.name }))
281         var total = 0
282         for (entry in list) {
283             val count = entry.value
284             val issue = entry.key
285             writer.println("    ${String.format("%5d", count)} ${String.format("%-30s", issue.name)} ${configuration.getSeverity(issue)}")
286             total += count
287         }
288         writer.println("" +
289             "    ---------------------------------------------\n" +
290             "    ${String.format("%5d", total)}")
291         writer.println()
292     }
293 
294     /**
295      * Builder for [Baseline]. [build] will return a non-null [Baseline] if either [file] or
296      * [updateFile] is set.
297      */
298     class Builder {
299         var description: String = ""
300 
301         var file: File? = null
302             set(value) {
303                 if (field != null) {
304                     throw DriverException("Only one baseline is allowed; found both $field and $value")
305                 }
306                 field = value
307             }
308         var merge: Boolean = false
309 
310         var updateFile: File? = null
311             set(value) {
312                 if (field != null) {
313                     throw DriverException("Only one update-baseline is allowed; found both $field and $value")
314                 }
315                 field = value
316             }
317 
318         var headerComment: String = ""
319 
320         fun build(): Baseline? {
321             // If neither file nor updateFile is set, don't return an instance.
322             if (file == null && updateFile == null) {
323                 return null
324             }
325             if (description.isEmpty()) {
326                 throw DriverException("Baseline description must be set")
327             }
328             return Baseline(description, file, updateFile, merge, headerComment)
329         }
330     }
331 }
332