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