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