1 /* <lambda>null2 * Copyright (C) 2017 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.Severity.ERROR 20 import com.android.tools.metalava.Severity.HIDDEN 21 import com.android.tools.metalava.Severity.INFO 22 import com.android.tools.metalava.Severity.INHERIT 23 import com.android.tools.metalava.Severity.LINT 24 import com.android.tools.metalava.Severity.WARNING 25 import com.android.tools.metalava.model.AnnotationArrayAttributeValue 26 import com.android.tools.metalava.model.Item 27 import com.android.tools.metalava.model.configuration 28 import com.android.tools.metalava.model.psi.PsiItem 29 import com.android.tools.metalava.model.text.TextItem 30 import com.google.common.annotations.VisibleForTesting 31 import com.intellij.openapi.util.TextRange 32 import com.intellij.openapi.vfs.VfsUtilCore 33 import com.intellij.psi.PsiCompiledElement 34 import com.intellij.psi.PsiElement 35 import com.intellij.psi.PsiModifierListOwner 36 import com.intellij.psi.impl.light.LightElement 37 import org.jetbrains.uast.kotlin.KotlinUClass 38 import java.io.File 39 import java.io.PrintWriter 40 41 /** 42 * "Global" [Reporter] used by most operations. 43 * Certain operations, such as api-lint and compatibility check, may use a custom [Reporter] 44 */ 45 lateinit var reporter: Reporter 46 47 enum class Severity(private val displayName: String) { 48 INHERIT("inherit"), 49 50 HIDDEN("hidden"), 51 52 /** 53 * Information level are for issues that are informational only; may or 54 * may not be a problem. 55 */ 56 INFO("info"), 57 58 /** 59 * Lint level means that we encountered inconsistent or broken documentation. 60 * These should be resolved, but don't impact API compatibility. 61 */ 62 LINT("lint"), 63 64 /** 65 * Warning level means that we encountered some incompatible or inconsistent 66 * API change. These must be resolved to preserve API compatibility. 67 */ 68 WARNING("warning"), 69 70 /** 71 * Error level means that we encountered severe trouble and were unable to 72 * output the requested documentation. 73 */ 74 ERROR("error"); 75 76 override fun toString(): String = displayName 77 } 78 79 class Reporter( 80 /** [Baseline] file associated with this [Reporter]. If null, the global baseline is used. */ 81 // See the comment on [getBaseline] for why it's nullable. 82 private val customBaseline: Baseline?, 83 84 /** 85 * An error message associated with this [Reporter], which should be shown to the user 86 * when metalava finishes with errors. 87 */ 88 private val errorMessage: String? 89 ) { 90 private var errors = mutableListOf<String>() 91 private var warningCount = 0 92 val totalCount get() = errors.size + warningCount 93 94 /** The number of errors. */ 95 val errorCount get() = errors.size 96 97 /** Returns whether any errors have been detected. */ hasErrorsnull98 fun hasErrors(): Boolean = errors.size > 0 99 100 // Note we can't set [options.baseline] as the default for [customBaseline], because 101 // options.baseline will be initialized after the global [Reporter] is instantiated. 102 private fun getBaseline(): Baseline? = customBaseline ?: options.baseline 103 104 fun report(id: Issues.Issue, element: PsiElement?, message: String): Boolean { 105 val severity = configuration.getSeverity(id) 106 107 if (severity == HIDDEN) { 108 return false 109 } 110 111 val baseline = getBaseline() 112 if (element != null && baseline != null && baseline.mark(element, message, id)) { 113 return false 114 } 115 116 return report(severity, elementToLocation(element), message, id) 117 } 118 reportnull119 fun report(id: Issues.Issue, file: File?, message: String): Boolean { 120 val severity = configuration.getSeverity(id) 121 122 if (severity == HIDDEN) { 123 return false 124 } 125 126 val baseline = getBaseline() 127 if (file != null && baseline != null && baseline.mark(file, message, id)) { 128 return false 129 } 130 131 return report(severity, file?.path, message, id) 132 } 133 reportnull134 fun report(id: Issues.Issue, item: Item?, message: String, psi: PsiElement? = null): Boolean { 135 val severity = configuration.getSeverity(id) 136 if (severity == HIDDEN) { 137 return false 138 } 139 140 fun dispatch( 141 which: (severity: Severity, location: String?, message: String, id: Issues.Issue) -> Boolean 142 ) = when { 143 psi != null -> which(severity, elementToLocation(psi), message, id) 144 item is PsiItem -> which(severity, elementToLocation(item.psi()), message, id) 145 item is TextItem -> 146 which(severity, (item as? TextItem)?.position.toString(), message, id) 147 else -> which(severity, null as String?, message, id) 148 } 149 150 // Optionally write to the --report-even-if-suppressed file. 151 dispatch(this::reportEvenIfSuppressed) 152 153 if (isSuppressed(id, item, message)) { 154 return false 155 } 156 157 // If we are only emitting some packages (--stub-packages), don't report 158 // issues from other packages 159 if (item != null) { 160 val packageFilter = options.stubPackages 161 if (packageFilter != null) { 162 val pkg = item.containingPackage(false) 163 if (pkg != null && !packageFilter.matches(pkg)) { 164 return false 165 } 166 } 167 } 168 169 val baseline = getBaseline() 170 if (item != null && baseline != null && baseline.mark(item, message, id)) { 171 return false 172 } else if (psi != null && baseline != null && baseline.mark(psi, message, id)) { 173 return false 174 } 175 176 return dispatch(this::doReport) 177 } 178 isSuppressednull179 fun isSuppressed(id: Issues.Issue, item: Item? = null, message: String? = null): Boolean { 180 val severity = configuration.getSeverity(id) 181 if (severity == HIDDEN) { 182 return true 183 } 184 185 item ?: return false 186 187 for (annotation in item.modifiers.annotations()) { 188 val annotationName = annotation.qualifiedName() 189 if (annotationName != null && annotationName in SUPPRESS_ANNOTATIONS) { 190 for (attribute in annotation.attributes()) { 191 // Assumption that all annotations in SUPPRESS_ANNOTATIONS only have 192 // one attribute such as value/names that is varargs of String 193 val value = attribute.value 194 if (value is AnnotationArrayAttributeValue) { 195 // Example: @SuppressLint({"RequiresFeature", "AllUpper"}) 196 for (innerValue in value.values) { 197 val string = innerValue.value()?.toString() ?: continue 198 if (suppressMatches(string, id.name, message)) { 199 return true 200 } 201 } 202 } else { 203 // Example: @SuppressLint("RequiresFeature") 204 val string = value.value()?.toString() 205 if (string != null && (suppressMatches(string, id.name, message))) { 206 return true 207 } 208 } 209 } 210 } 211 } 212 213 return false 214 } 215 suppressMatchesnull216 private fun suppressMatches(value: String, id: String?, message: String?): Boolean { 217 id ?: return false 218 219 if (value == id) { 220 return true 221 } 222 223 if (message != null && value.startsWith(id) && value.endsWith(message) && 224 (value == "$id:$message" || value == "$id: $message") 225 ) { 226 return true 227 } 228 229 return false 230 } 231 getTextRangenull232 private fun getTextRange(element: PsiElement): TextRange? { 233 var range: TextRange? = null 234 235 if (element is KotlinUClass) { 236 range = element.sourcePsi?.textRange 237 } else if (element is PsiCompiledElement) { 238 if (element is LightElement) { 239 range = (element as PsiElement).textRange 240 } 241 if (range == null || TextRange.EMPTY_RANGE == range) { 242 return null 243 } 244 } else { 245 range = element.textRange 246 } 247 248 return range 249 } 250 elementToLocationnull251 private fun elementToLocation(element: PsiElement?, includeDocs: Boolean = true): String? { 252 element ?: return null 253 val psiFile = element.containingFile ?: return null 254 val virtualFile = psiFile.virtualFile ?: return null 255 val file = VfsUtilCore.virtualToIoFile(virtualFile) 256 257 val path = (rootFolder?.toPath()?.relativize(file.toPath()) ?: file.toPath()).toString() 258 259 // Skip doc comments for classes, methods and fields; we usually want to point right to 260 // the class/method/field definition 261 val rangeElement = if (!includeDocs && element is PsiModifierListOwner) { 262 element.modifierList ?: element 263 } else 264 element 265 266 val range = getTextRange(rangeElement) 267 val lineNumber = if (range == null) { 268 // No source offsets, use invalid line number 269 -1 270 } else { 271 getLineNumber(psiFile.text, range.startOffset) + 1 272 } 273 return if (lineNumber > 0) "$path:$lineNumber" else path 274 } 275 276 /** Returns the 0-based line number of character position <offset> in <text> */ getLineNumbernull277 private fun getLineNumber(text: String, offset: Int): Int { 278 var line = 0 279 var curr = 0 280 val target = offset.coerceAtMost(text.length) 281 while (curr < target) { 282 if (text[curr++] == '\n') { 283 line++ 284 } 285 } 286 return line 287 } 288 289 /** Alias to allow method reference to `dispatch` in [report] */ doReportnull290 private fun doReport(severity: Severity, location: String?, message: String, id: Issues.Issue?) = 291 report(severity, location, message, id) 292 293 fun report( 294 severity: Severity, 295 location: String?, 296 message: String, 297 id: Issues.Issue? = null, 298 color: Boolean = options.color 299 ): Boolean { 300 if (severity == HIDDEN) { 301 return false 302 } 303 304 val effectiveSeverity = 305 if (severity == LINT && options.lintsAreErrors) 306 ERROR 307 else if (severity == WARNING && options.warningsAreErrors) { 308 ERROR 309 } else { 310 severity 311 } 312 313 val formattedMessage = format(effectiveSeverity, location, message, id, color, options.omitLocations) 314 if (effectiveSeverity == ERROR) { 315 errors.add(formattedMessage) 316 } else if (severity == WARNING) { 317 warningCount++ 318 } 319 320 reportPrinter(formattedMessage, effectiveSeverity) 321 return true 322 } 323 formatnull324 private fun format( 325 severity: Severity, 326 location: String?, 327 message: String, 328 id: Issues.Issue?, 329 color: Boolean, 330 omitLocations: Boolean 331 ): String { 332 val sb = StringBuilder(100) 333 334 if (color && !isUnderTest()) { 335 sb.append(terminalAttributes(bold = true)) 336 if (!omitLocations) { 337 location?.let { 338 sb.append(it).append(": ") 339 } 340 } 341 when (severity) { 342 LINT -> sb.append(terminalAttributes(foreground = TerminalColor.CYAN)).append("lint: ") 343 INFO -> sb.append(terminalAttributes(foreground = TerminalColor.CYAN)).append("info: ") 344 WARNING -> sb.append(terminalAttributes(foreground = TerminalColor.YELLOW)).append("warning: ") 345 ERROR -> sb.append(terminalAttributes(foreground = TerminalColor.RED)).append("error: ") 346 INHERIT, HIDDEN -> { 347 } 348 } 349 sb.append(resetTerminal()) 350 sb.append(message) 351 id?.let { 352 sb.append(" [").append(it.name).append("]") 353 } 354 } else { 355 if (!omitLocations) { 356 location?.let { sb.append(it).append(": ") } 357 } 358 when (severity) { 359 LINT -> sb.append("lint: ") 360 INFO -> sb.append("info: ") 361 WARNING -> sb.append("warning: ") 362 ERROR -> sb.append("error: ") 363 INHERIT, HIDDEN -> { 364 } 365 } 366 sb.append(message) 367 id?.let { 368 sb.append(" [") 369 sb.append(it.name) 370 sb.append("]") 371 val link = it.category.ruleLink 372 if (it.rule != null && link != null) { 373 sb.append(" [See ").append(link).append(it.rule) 374 sb.append("]") 375 } 376 } 377 } 378 return sb.toString() 379 } 380 reportEvenIfSuppressednull381 private fun reportEvenIfSuppressed( 382 severity: Severity, 383 location: String?, 384 message: String, 385 id: Issues.Issue 386 ): Boolean { 387 options.reportEvenIfSuppressedWriter?.println( 388 format( 389 severity, 390 location, 391 message, 392 id, 393 color = false, 394 omitLocations = false 395 )) 396 return true 397 } 398 399 /** 400 * Print all the recorded errors to the given writer. Returns the number of errors printer. 401 */ printErrorsnull402 fun printErrors(writer: PrintWriter, maxErrors: Int): Int { 403 var i = 0 404 errors.forEach loop@{ 405 if (i >= maxErrors) { 406 return@loop 407 } 408 i++ 409 writer.println(it) 410 } 411 return i 412 } 413 414 /** Write the error message set to this [Reporter], if any errors have been detected. */ writeErrorMessagenull415 fun writeErrorMessage(writer: PrintWriter) { 416 if (hasErrors()) { 417 errorMessage ?. let { writer.write(it) } 418 } 419 } 420 getBaselineDescriptionnull421 fun getBaselineDescription(): String { 422 val file = getBaseline()?.file 423 return if (file != null) { 424 "baseline ${file.path}" 425 } else { 426 "no baseline" 427 } 428 } 429 430 companion object { 431 /** root folder, which needs to be changed for unit tests. */ 432 @VisibleForTesting 433 internal var rootFolder: File? = File("").absoluteFile 434 435 /** Injection point for unit tests. */ severitynull436 internal var reportPrinter: (String, Severity) -> Unit = { message, severity -> 437 val output = if (severity == ERROR) { 438 options.stderr 439 } else { 440 options.stdout 441 } 442 output.println() 443 output.print(message.trim()) 444 output.flush() 445 } 446 } 447 } 448 449 private val SUPPRESS_ANNOTATIONS = listOf( 450 ANDROID_SUPPRESS_LINT, 451 JAVA_LANG_SUPPRESS_WARNINGS, 452 KOTLIN_SUPPRESS 453 )