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.doclava1.Issues 26 import com.android.tools.metalava.model.AnnotationArrayAttributeValue 27 import com.android.tools.metalava.model.Item 28 import com.android.tools.metalava.model.configuration 29 import com.android.tools.metalava.model.psi.PsiItem 30 import com.android.tools.metalava.model.text.TextItem 31 import com.google.common.annotations.VisibleForTesting 32 import com.intellij.openapi.util.TextRange 33 import com.intellij.openapi.vfs.VfsUtilCore 34 import com.intellij.psi.PsiCompiledElement 35 import com.intellij.psi.PsiElement 36 import com.intellij.psi.PsiModifierListOwner 37 import com.intellij.psi.impl.light.LightElement 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 if (severity == LINT || severity == WARNING || severity == ERROR) { 188 for (annotation in item.modifiers.annotations()) { 189 val annotationName = annotation.qualifiedName() 190 if (annotationName != null && annotationName in SUPPRESS_ANNOTATIONS) { 191 for (attribute in annotation.attributes()) { 192 val id1 = "Doclava${id.code}" 193 val id2 = id.name 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({"DocLava1", "DocLava2"}) 199 for (innerValue in value.values) { 200 val string = innerValue.value()?.toString() ?: continue 201 if (suppressMatches(string, id1, message) || suppressMatches(string, id2, message)) { 202 return true 203 } 204 } 205 } else { 206 // Example: @SuppressLint("DocLava1") 207 val string = value.value()?.toString() 208 if (string != null && ( 209 suppressMatches(string, id1, message) || suppressMatches(string, id2, message)) 210 ) { 211 return true 212 } 213 } 214 } 215 } 216 } 217 } 218 219 return false 220 } 221 suppressMatchesnull222 private fun suppressMatches(value: String, id: String?, message: String?): Boolean { 223 id ?: return false 224 225 if (value == id) { 226 return true 227 } 228 229 if (message != null && value.startsWith(id) && value.endsWith(message) && 230 (value == "$id:$message" || value == "$id: $message") 231 ) { 232 return true 233 } 234 235 return false 236 } 237 getTextRangenull238 private fun getTextRange(element: PsiElement): TextRange? { 239 var range: TextRange? = null 240 241 if (element is PsiCompiledElement) { 242 if (element is LightElement) { 243 range = (element as PsiElement).textRange 244 } 245 if (range == null || TextRange.EMPTY_RANGE == range) { 246 return null 247 } 248 } else { 249 range = element.textRange 250 } 251 252 return range 253 } 254 elementToLocationnull255 private fun elementToLocation(element: PsiElement?, includeDocs: Boolean = true): String? { 256 element ?: return null 257 val psiFile = element.containingFile ?: return null 258 val virtualFile = psiFile.virtualFile ?: return null 259 val file = VfsUtilCore.virtualToIoFile(virtualFile) 260 261 val path = (rootFolder?.toPath()?.relativize(file.toPath()) ?: file.toPath()).toString() 262 263 // Skip doc comments for classes, methods and fields; we usually want to point right to 264 // the class/method/field definition 265 val rangeElement = if (!includeDocs && element is PsiModifierListOwner) { 266 element.modifierList ?: element 267 } else 268 element 269 270 val range = getTextRange(rangeElement) 271 val lineNumber = if (range == null) { 272 // No source offsets, use invalid line number 273 -1 274 } else { 275 getLineNumber(psiFile.text, range.startOffset) + 1 276 } 277 return if (lineNumber > 0) "$path:$lineNumber" else path 278 } 279 280 /** Returns the 0-based line number of character position <offset> in <text> */ getLineNumbernull281 private fun getLineNumber(text: String, offset: Int): Int { 282 var line = 0 283 var curr = 0 284 val target = offset.coerceAtMost(text.length) 285 while (curr < target) { 286 if (text[curr++] == '\n') { 287 line++ 288 } 289 } 290 return line 291 } 292 293 /** Alias to allow method reference to `dispatch` in [report] */ doReportnull294 private fun doReport(severity: Severity, location: String?, message: String, id: Issues.Issue?) = 295 report(severity, location, message, id) 296 297 fun report( 298 severity: Severity, 299 location: String?, 300 message: String, 301 id: Issues.Issue? = null, 302 color: Boolean = options.color 303 ): Boolean { 304 if (severity == HIDDEN) { 305 return false 306 } 307 308 val effectiveSeverity = 309 if (severity == LINT && options.lintsAreErrors) 310 ERROR 311 else if (severity == WARNING && options.warningsAreErrors) { 312 ERROR 313 } else { 314 severity 315 } 316 317 val formattedMessage = format(effectiveSeverity, location, message, id, color, options.omitLocations) 318 if (effectiveSeverity == ERROR) { 319 errors.add(formattedMessage) 320 } else if (severity == WARNING) { 321 warningCount++ 322 } 323 324 reportPrinter(formattedMessage, effectiveSeverity) 325 return true 326 } 327 formatnull328 private fun format( 329 severity: Severity, 330 location: String?, 331 message: String, 332 id: Issues.Issue?, 333 color: Boolean, 334 omitLocations: Boolean 335 ): String { 336 val sb = StringBuilder(100) 337 338 if (color && !isUnderTest()) { 339 sb.append(terminalAttributes(bold = true)) 340 if (!omitLocations) { 341 location?.let { 342 sb.append(it).append(": ") 343 } 344 } 345 when (severity) { 346 LINT -> sb.append(terminalAttributes(foreground = TerminalColor.CYAN)).append("lint: ") 347 INFO -> sb.append(terminalAttributes(foreground = TerminalColor.CYAN)).append("info: ") 348 WARNING -> sb.append(terminalAttributes(foreground = TerminalColor.YELLOW)).append("warning: ") 349 ERROR -> sb.append(terminalAttributes(foreground = TerminalColor.RED)).append("error: ") 350 INHERIT, HIDDEN -> { 351 } 352 } 353 sb.append(resetTerminal()) 354 sb.append(message) 355 id?.let { 356 sb.append(" [").append(it.name).append("]") 357 } 358 } else { 359 if (!omitLocations) { 360 location?.let { sb.append(it).append(": ") } 361 } 362 if (compatibility.oldErrorOutputFormat) { 363 // according to doclava1 there are some people or tools parsing old format 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 id?.let { sb.append(it.name).append(": ") } 373 sb.append(message) 374 } else { 375 when (severity) { 376 LINT -> sb.append("lint: ") 377 INFO -> sb.append("info: ") 378 WARNING -> sb.append("warning: ") 379 ERROR -> sb.append("error: ") 380 INHERIT, HIDDEN -> { 381 } 382 } 383 sb.append(message) 384 id?.let { 385 sb.append(" [") 386 sb.append(it.name) 387 if (compatibility.includeExitCode) { 388 sb.append(":") 389 sb.append(it.code) 390 } 391 sb.append("]") 392 if (it.rule != null) { 393 sb.append(" [Rule ").append(it.rule) 394 val link = it.category.ruleLink 395 if (link != null) { 396 sb.append(" in ").append(link) 397 } 398 sb.append("]") 399 } 400 } 401 } 402 } 403 return sb.toString() 404 } 405 reportEvenIfSuppressednull406 private fun reportEvenIfSuppressed( 407 severity: Severity, 408 location: String?, 409 message: String, 410 id: Issues.Issue 411 ): Boolean { 412 options.reportEvenIfSuppressedWriter?.println( 413 format( 414 severity, 415 location, 416 message, 417 id, 418 color = false, 419 omitLocations = false 420 )) 421 return true 422 } 423 424 /** 425 * Print all the recorded errors to the given writer. Returns the number of errors printer. 426 */ printErrorsnull427 fun printErrors(writer: PrintWriter, maxErrors: Int): Int { 428 var i = 0 429 errors.forEach loop@{ 430 if (i >= maxErrors) { 431 return@loop 432 } 433 i++ 434 writer.println(it) 435 } 436 return i 437 } 438 439 /** Write the error message set to this [Reporter], if any errors have been detected. */ writeErrorMessagenull440 fun writeErrorMessage(writer: PrintWriter) { 441 if (hasErrors()) { 442 errorMessage ?. let { writer.write(it) } 443 } 444 } 445 getBaselineDescriptionnull446 fun getBaselineDescription(): String { 447 val file = getBaseline()?.file 448 return if (file != null) { 449 "baseline ${file.path}" 450 } else { 451 "no baseline" 452 } 453 } 454 455 companion object { 456 /** root folder, which needs to be changed for unit tests. */ 457 @VisibleForTesting 458 internal var rootFolder: File? = File("").absoluteFile 459 460 /** Injection point for unit tests. */ severitynull461 internal var reportPrinter: (String, Severity) -> Unit = { message, severity -> 462 val output = if (severity == ERROR) { 463 options.stderr 464 } else { 465 options.stdout 466 } 467 output.println() 468 output.print(message.trim()) 469 output.flush() 470 } 471 } 472 } 473 474 private val SUPPRESS_ANNOTATIONS = listOf( 475 ANDROID_SUPPRESS_LINT, 476 JAVA_LANG_SUPPRESS_WARNINGS, 477 KOTLIN_SUPPRESS 478 )