• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.model.ClassItem
20 import com.android.tools.metalava.model.FieldItem
21 import com.android.tools.metalava.model.Item
22 import com.android.tools.metalava.model.MethodItem
23 import com.android.tools.metalava.model.PackageItem
24 import com.android.tools.metalava.model.ParameterItem
25 import com.android.tools.metalava.model.configuration
26 import com.intellij.openapi.vfs.VfsUtilCore
27 import com.intellij.psi.PsiClass
28 import com.intellij.psi.PsiElement
29 import com.intellij.psi.PsiField
30 import com.intellij.psi.PsiFile
31 import com.intellij.psi.PsiMethod
32 import com.intellij.psi.PsiPackage
33 import com.intellij.psi.PsiParameter
34 import org.jetbrains.kotlin.psi.KtClass
35 import org.jetbrains.kotlin.psi.KtProperty
36 import org.jetbrains.kotlin.psi.psiUtil.containingClass
37 import org.jetbrains.kotlin.psi.psiUtil.parameterIndex
38 import java.io.File
39 import java.io.PrintWriter
40 import kotlin.text.Charsets.UTF_8
41 
42 const val DEFAULT_BASELINE_NAME = "baseline.txt"
43 
44 class Baseline(
45     /** Description of this baseline. e.g. "api-lint. */
46     val description: String,
47     val file: File?,
48     var updateFile: File?,
49     // TODO(roosa): unless file == updateFile, existing baselines will be merged into the updateFile regardless of this value
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             ) {
209                 continue
210             }
211             val idEnd = line.indexOf(':')
212             val elementEnd = line.indexOf(':', idEnd + 1)
213             if (idEnd == -1 || elementEnd == -1) {
214                 println("Invalid metalava baseline format: $line")
215             }
216             val issueId = line.substring(0, idEnd).trim()
217             val elementId = line.substring(idEnd + 2, elementEnd).trim()
218 
219             val message = lines[i + 1].trim()
220 
221             val issue = Issues.findIssueById(issueId)
222             if (issue == null) {
223                 println("Invalid metalava baseline file: unknown issue id '$issueId'")
224             } else {
225                 val newIdMap = map[issue] ?: run {
226                     val new = HashMap<String, String>()
227                     map[issue] = new
228                     new
229                 }
230                 newIdMap[elementId] = message
231             }
232         }
233     }
234 
235     private fun write(): Boolean {
236         val updateFile = this.updateFile ?: return false
237         if (map.isNotEmpty() || !options.deleteEmptyBaselines) {
238             val sb = StringBuilder()
239             sb.append(format.header())
240             sb.append(headerComment)
241 
242             map.keys.asSequence().sortedBy { it.name }.forEach { issue ->
243                 val idMap = map[issue]
244                 idMap?.keys?.sorted()?.forEach { elementId ->
245                     val message = idMap[elementId]!!
246                     sb.append(issue.name).append(": ")
247                     sb.append(elementId)
248                     sb.append(":\n    ")
249                     sb.append(message).append('\n')
250                 }
251                 sb.append("\n\n")
252             }
253 
254             if (sb.endsWith("\n\n")) {
255                 sb.setLength(sb.length - 2)
256             }
257 
258             updateFile.parentFile?.mkdirs()
259             updateFile.writeText(sb.toString(), UTF_8)
260         } else {
261             updateFile.delete()
262         }
263         return true
264     }
265 
266     fun dumpStats(writer: PrintWriter) {
267         val counts = mutableMapOf<Issues.Issue, Int>()
268         map.keys.asSequence().forEach { issue ->
269             val idMap = map[issue]
270             val count = idMap?.count() ?: 0
271             counts[issue] = count
272         }
273 
274         writer.println("Baseline issue type counts for $description baseline:")
275         writer.println(
276             "" +
277                 "    Count Issue Id                       Severity\n" +
278                 "    ---------------------------------------------\n"
279         )
280         val list = counts.entries.toMutableList()
281         list.sortWith(compareBy({ -it.value }, { it.key.name }))
282         var total = 0
283         for (entry in list) {
284             val count = entry.value
285             val issue = entry.key
286             writer.println("    ${String.format("%5d", count)} ${String.format("%-30s", issue.name)} ${configuration.getSeverity(issue)}")
287             total += count
288         }
289         writer.println(
290             "" +
291                 "    ---------------------------------------------\n" +
292                 "    ${String.format("%5d", total)}"
293         )
294         writer.println()
295     }
296 
297     /**
298      * Builder for [Baseline]. [build] will return a non-null [Baseline] if either [file] or
299      * [updateFile] is set.
300      */
301     class Builder {
302         var description: String = ""
303 
304         var file: File? = null
305             set(value) {
306                 if (field != null) {
307                     throw DriverException("Only one baseline is allowed; found both $field and $value")
308                 }
309                 field = value
310             }
311         var merge: Boolean = false
312 
313         var updateFile: File? = null
314             set(value) {
315                 if (field != null) {
316                     throw DriverException("Only one update-baseline is allowed; found both $field and $value")
317                 }
318                 field = value
319             }
320 
321         var headerComment: String = ""
322 
323         fun build(): Baseline? {
324             // If neither file nor updateFile is set, don't return an instance.
325             if (file == null && updateFile == null) {
326                 return null
327             }
328             if (description.isEmpty()) {
329                 throw DriverException("Baseline description must be set")
330             }
331             return Baseline(description, file, updateFile, merge, headerComment)
332         }
333     }
334 }
335