• 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                 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             val message = lines[i + 1].trim()
219 
220             val issue = Issues.findIssueById(issueId)
221             if (issue == null) {
222                 println("Invalid metalava baseline file: unknown issue id '$issueId'")
223             } else {
224                 val newIdMap = map[issue] ?: run {
225                     val new = HashMap<String, String>()
226                     map[issue] = new
227                     new
228                 }
229                 newIdMap[elementId] = message
230             }
231         }
232     }
233 
234     private fun write(): Boolean {
235         val updateFile = this.updateFile ?: return false
236         if (map.isNotEmpty() || !options.deleteEmptyBaselines) {
237             val sb = StringBuilder()
238             sb.append(format.header())
239             sb.append(headerComment)
240 
241             map.keys.asSequence().sortedBy { it.name }.forEach { issue ->
242                 val idMap = map[issue]
243                 idMap?.keys?.sorted()?.forEach { elementId ->
244                     val message = idMap[elementId]!!
245                     sb.append(issue.name).append(": ")
246                     sb.append(elementId)
247                     sb.append(":\n    ")
248                     sb.append(message).append('\n')
249                 }
250                 sb.append("\n\n")
251             }
252 
253             if (sb.endsWith("\n\n")) {
254                 sb.setLength(sb.length - 2)
255             }
256 
257             updateFile.parentFile?.mkdirs()
258             updateFile.writeText(sb.toString(), UTF_8)
259         } else {
260             updateFile.delete()
261         }
262         return true
263     }
264 
265     fun dumpStats(writer: PrintWriter) {
266         val counts = mutableMapOf<Issues.Issue, Int>()
267         map.keys.asSequence().forEach { issue ->
268             val idMap = map[issue]
269             val count = idMap?.count() ?: 0
270             counts[issue] = count
271         }
272 
273         writer.println("Baseline issue type counts for $description baseline:")
274         writer.println("" +
275             "    Count Issue Id                       Severity\n" +
276             "    ---------------------------------------------\n")
277         val list = counts.entries.toMutableList()
278         list.sortWith(compareBy({ -it.value }, { it.key.name }))
279         var total = 0
280         for (entry in list) {
281             val count = entry.value
282             val issue = entry.key
283             writer.println("    ${String.format("%5d", count)} ${String.format("%-30s", issue.name)} ${configuration.getSeverity(issue)}")
284             total += count
285         }
286         writer.println("" +
287             "    ---------------------------------------------\n" +
288             "    ${String.format("%5d", total)}")
289         writer.println()
290     }
291 
292     /**
293      * Builder for [Baseline]. [build] will return a non-null [Baseline] if either [file] or
294      * [updateFile] is set.
295      */
296     class Builder {
297         var description: String = ""
298 
299         var file: File? = null
300             set(value) {
301                 if (field != null) {
302                     throw DriverException("Only one baseline is allowed; found both $field and $value")
303                 }
304                 field = value
305             }
306         var merge: Boolean = false
307 
308         var updateFile: File? = null
309             set(value) {
310                 if (field != null) {
311                     throw DriverException("Only one update-baseline is allowed; found both $field and $value")
312                 }
313                 field = value
314             }
315 
316         var headerComment: String = ""
317 
318         fun build(): Baseline? {
319             // If neither file nor updateFile is set, don't return an instance.
320             if (file == null && updateFile == null) {
321                 return null
322             }
323             if (description.isEmpty()) {
324                 throw DriverException("Baseline description must be set")
325             }
326             return Baseline(description, file, updateFile, merge, headerComment)
327         }
328     }
329 }
330