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