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 }