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