• 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.doclava1.Errors
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.psiUtil.parameterIndex
36 import java.io.File
37 import java.io.PrintWriter
38 import kotlin.text.Charsets.UTF_8
39 
40 const val DEFAULT_BASELINE_NAME = "baseline.txt"
41 
42 class Baseline(
43     val file: File?,
44     var updateFile: File?,
45     var merge: Boolean = false,
46     private var headerComment: String = "",
47     /**
48      * Whether, when updating the baseline, we should fail the build if the main baseline does not
49      * contain all errors.
50      */
51     var silentUpdate: Boolean = updateFile != null && updateFile.path == file?.path,
52     private var format: FileFormat = FileFormat.BASELINE
53 ) {
54 
55     /** Map from issue id to element id to message */
56     private val map = HashMap<Errors.Error, MutableMap<String, String>>()
57 
58     init {
59         if (file?.isFile == true && (!silentUpdate || merge)) {
60             // We've set a baseline for a nonexistent file: read it
61             read()
62         }
63     }
64 
65     /** Returns true if the given error is listed in the baseline, otherwise false */
66     fun mark(element: Item, message: String, error: Errors.Error): Boolean {
67         val elementId = getBaselineKey(element)
68         return mark(elementId, message, error)
69     }
70 
71     /** Returns true if the given error is listed in the baseline, otherwise false */
72     fun mark(element: PsiElement, message: String, error: Errors.Error): Boolean {
73         val elementId = getBaselineKey(element)
74         return mark(elementId, message, error)
75     }
76 
77     /** Returns true if the given error is listed in the baseline, otherwise false */
78     fun mark(file: File, message: String, error: Errors.Error): Boolean {
79         val elementId = getBaselineKey(file)
80         return mark(elementId, message, error)
81     }
82 
83     private fun mark(elementId: String, @Suppress("UNUSED_PARAMETER") message: String, error: Errors.Error): Boolean {
84         val idMap: MutableMap<String, String>? = map[error] ?: run {
85             if (updateFile != null) {
86                 if (options.baselineErrorsOnly && configuration.getSeverity(error) != Severity.ERROR) {
87                     return true
88                 }
89                 val new = HashMap<String, String>()
90                 map[error] = new
91                 new
92             } else {
93                 null
94             }
95         }
96 
97         val oldMessage: String? = idMap?.get(elementId)
98         if (oldMessage != null) {
99             // for now not matching messages; the id's are unique enough and allows us
100             // to tweak error messages compatibly without recording all the deltas here
101             return true
102         }
103 
104         if (updateFile != null) {
105             idMap?.set(elementId, message)
106 
107             // When creating baselines don't report errors
108             if (silentUpdate) {
109                 return true
110             }
111         }
112 
113         return false
114     }
115 
116     private fun getBaselineKey(element: Item): String {
117         return when (element) {
118             is ClassItem -> element.qualifiedName()
119             is MethodItem -> element.containingClass().qualifiedName() + "#" +
120                 element.name() + "(" + element.parameters().joinToString { it.type().toSimpleType() } + ")"
121             is FieldItem -> element.containingClass().qualifiedName() + "#" + element.name()
122             is PackageItem -> element.qualifiedName()
123             is ParameterItem -> getBaselineKey(element.containingMethod()) + " parameter #" + element.parameterIndex
124             else -> element.describe(false)
125         }
126     }
127 
128     private fun getBaselineKey(element: PsiElement): String {
129         return when (element) {
130             is PsiClass -> element.qualifiedName ?: element.name ?: "?"
131             is PsiMethod -> {
132                 val containingClass = element.containingClass
133                 val name = element.name
134                 val parameterList = "(" + element.parameterList.parameters.joinToString { it.type.canonicalText } + ")"
135                 if (containingClass != null) {
136                     getBaselineKey(containingClass) + "#" + name + parameterList
137                 } else {
138                     name + parameterList
139                 }
140             }
141             is PsiField -> {
142                 val containingClass = element.containingClass
143                 val name = element.name
144                 if (containingClass != null) {
145                     getBaselineKey(containingClass) + "#" + name
146                 } else {
147                     name
148                 }
149             }
150             is PsiPackage -> element.qualifiedName
151             is PsiParameter -> {
152                 val method = element.declarationScope.parent
153                 if (method is PsiMethod) {
154                     getBaselineKey(method) + " parameter #" + element.parameterIndex()
155                 } else {
156                     "?"
157                 }
158             }
159             is PsiFile -> {
160                 val virtualFile = element.virtualFile
161                 val file = VfsUtilCore.virtualToIoFile(virtualFile)
162                 return getBaselineKey(file)
163             }
164             else -> element.toString()
165         }
166     }
167 
168     private fun getBaselineKey(file: File): String {
169         val path = file.path
170         for (sourcePath in options.sourcePath) {
171             if (path.startsWith(sourcePath.path)) {
172                 return path.substring(sourcePath.path.length).replace('\\', '/').removePrefix("/")
173             }
174         }
175 
176         return path.replace('\\', '/')
177     }
178 
179     fun close() {
180         write()
181     }
182 
183     private fun read() {
184         val file = this.file ?: return
185         val lines = file.readLines(UTF_8)
186         for (i in 0 until lines.size - 1) {
187             val line = lines[i]
188             if (line.startsWith("//") ||
189                 line.startsWith("#") ||
190                 line.isBlank() ||
191                 line.startsWith(" ")) {
192                 continue
193             }
194             val idEnd = line.indexOf(':')
195             val elementEnd = line.indexOf(':', idEnd + 1)
196             if (idEnd == -1 || elementEnd == -1) {
197                 println("Invalid metalava baseline format: $line")
198             }
199             val errorId = line.substring(0, idEnd).trim()
200             val elementId = line.substring(idEnd + 2, elementEnd).trim()
201 
202             // Unless merging, we don't need the actual messages since we're only matching by
203             // issue id and API location, so don't bother computing.
204             val message = if (merge) lines[i + 1].trim() else ""
205 
206             val error = Errors.findErrorById(errorId)
207             if (error == null) {
208                 println("Invalid metalava baseline file: unknown error id '$errorId'")
209             } else {
210                 val newIdMap = map[error] ?: run {
211                     val new = HashMap<String, String>()
212                     map[error] = new
213                     new
214                 }
215                 newIdMap[elementId] = message
216             }
217         }
218     }
219 
220     private fun write() {
221         val updateFile = this.updateFile ?: return
222         if (!map.isEmpty() || !options.deleteEmptyBaselines) {
223             val sb = StringBuilder()
224             sb.append(format.header())
225             sb.append(headerComment)
226 
227             map.keys.asSequence().sortedBy { it.name ?: it.code.toString() }.forEach { error ->
228                 val idMap = map[error]
229                 idMap?.keys?.sorted()?.forEach { elementId ->
230                     val message = idMap[elementId]!!
231                     sb.append(error.name ?: error.code.toString()).append(": ")
232                     sb.append(elementId)
233                     sb.append(":\n    ")
234                     sb.append(message).append('\n')
235                 }
236                 sb.append("\n\n")
237             }
238 
239             if (sb.endsWith("\n\n")) {
240                 sb.setLength(sb.length - 2)
241             }
242 
243             updateFile.parentFile?.mkdirs()
244             updateFile.writeText(sb.toString(), UTF_8)
245         } else {
246             updateFile.delete()
247         }
248     }
249 
250     fun dumpStats(writer: PrintWriter) {
251         val counts = mutableMapOf<Errors.Error, Int>()
252         map.keys.asSequence().forEach { error ->
253             val idMap = map[error]
254             val count = idMap?.count() ?: 0
255             counts[error] = count
256         }
257 
258         writer.println("Baseline issue type counts:")
259         writer.println("" +
260             "    Count Issue Id                       Severity\n" +
261             "    ---------------------------------------------\n")
262         val list = counts.entries.toMutableList()
263         list.sortWith(compareBy({ -it.value }, { it.key.name ?: it.key.code.toString() }))
264         var total = 0
265         for (entry in list) {
266             val count = entry.value
267             val issue = entry.key
268             writer.println("    ${String.format("%5d", count)} ${String.format("%-30s", issue.name)} ${issue.level}")
269             total += count
270         }
271         writer.println("" +
272             "    ---------------------------------------------\n" +
273             "    ${String.format("%5d", total)}")
274         writer.println()
275     }
276 }