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.SdkConstants.ATTR_VALUE 20 import com.android.tools.metalava.Severity.ERROR 21 import com.android.tools.metalava.Severity.HIDDEN 22 import com.android.tools.metalava.Severity.INFO 23 import com.android.tools.metalava.Severity.INHERIT 24 import com.android.tools.metalava.Severity.LINT 25 import com.android.tools.metalava.Severity.WARNING 26 import com.android.tools.metalava.doclava1.Issues 27 import com.android.tools.metalava.model.AnnotationArrayAttributeValue 28 import com.android.tools.metalava.model.Item 29 import com.android.tools.metalava.model.configuration 30 import com.android.tools.metalava.model.psi.PsiItem 31 import com.android.tools.metalava.model.text.TextItem 32 import com.google.common.annotations.VisibleForTesting 33 import com.intellij.openapi.util.TextRange 34 import com.intellij.openapi.vfs.VfsUtilCore 35 import com.intellij.psi.PsiCompiledElement 36 import com.intellij.psi.PsiElement 37 import com.intellij.psi.PsiModifierListOwner 38 import com.intellij.psi.impl.light.LightElement 39 import java.io.File 40 import java.io.PrintWriter 41 42 /** 43 * "Global" [Reporter] used by most operations. 44 * Certain operations, such as api-lint and compatibility check, may use a custom [Reporter] 45 */ 46 lateinit var reporter: Reporter 47 48 enum class Severity(private val displayName: String) { 49 INHERIT("inherit"), 50 51 HIDDEN("hidden"), 52 53 /** 54 * Information level are for issues that are informational only; may or 55 * may not be a problem. 56 */ 57 INFO("info"), 58 59 /** 60 * Lint level means that we encountered inconsistent or broken documentation. 61 * These should be resolved, but don't impact API compatibility. 62 */ 63 LINT("lint"), 64 65 /** 66 * Warning level means that we encountered some incompatible or inconsistent 67 * API change. These must be resolved to preserve API compatibility. 68 */ 69 WARNING("warning"), 70 71 /** 72 * Error level means that we encountered severe trouble and were unable to 73 * output the requested documentation. 74 */ 75 ERROR("error"); 76 77 override fun toString(): String = displayName 78 } 79 80 class Reporter( 81 /** [Baseline] file associated with this [Reporter]. If null, the global baseline is used. */ 82 // See the comment on [getBaseline] for why it's nullable. 83 private val customBaseline: Baseline?, 84 85 /** 86 * An error message associated with this [Reporter], which should be shown to the user 87 * when metalava finishes with errors. 88 */ 89 private val errorMessage: String? 90 ) { 91 var errorCount = 0 92 private set 93 var warningCount = 0 94 private set 95 val totalCount get() = errorCount + warningCount 96 97 private var hasErrors = false 98 99 // Note we can't set [options.baseline] as the default for [customBaseline], because 100 // options.baseline will be initialized after the global [Reporter] is instantiated. getBaselinenull101 fun getBaseline(): Baseline? = customBaseline ?: options.baseline 102 103 fun report(id: Issues.Issue, element: PsiElement?, message: String): Boolean { 104 val severity = configuration.getSeverity(id) 105 106 if (severity == HIDDEN) { 107 return false 108 } 109 110 val baseline = getBaseline() 111 if (element != null && baseline != null && baseline.mark(element, message, id)) { 112 return false 113 } 114 115 return report(severity, elementToLocation(element), message, id) 116 } 117 reportnull118 fun report(id: Issues.Issue, file: File?, message: String): Boolean { 119 val severity = configuration.getSeverity(id) 120 121 if (severity == HIDDEN) { 122 return false 123 } 124 125 val baseline = getBaseline() 126 if (file != null && baseline != null && baseline.mark(file, message, id)) { 127 return false 128 } 129 130 return report(severity, file?.path, message, id) 131 } 132 reportnull133 fun report(id: Issues.Issue, item: Item?, message: String, psi: PsiElement? = null): Boolean { 134 val severity = configuration.getSeverity(id) 135 if (severity == HIDDEN) { 136 return false 137 } 138 139 fun dispatch( 140 which: (severity: Severity, location: String?, message: String, id: Issues.Issue) -> Boolean 141 ) = when { 142 psi != null -> which(severity, elementToLocation(psi), message, id) 143 item is PsiItem -> which(severity, elementToLocation(item.psi()), message, id) 144 item is TextItem -> 145 which(severity, (item as? TextItem)?.position.toString(), message, id) 146 else -> which(severity, null as String?, message, id) 147 } 148 149 // Optionally write to the --report-even-if-suppressed file. 150 dispatch(this::reportEvenIfSuppressed) 151 152 if (isSuppressed(id, item, message)) { 153 return false 154 } 155 156 // If we are only emitting some packages (--stub-packages), don't report 157 // issues from other packages 158 if (item != null) { 159 val packageFilter = options.stubPackages 160 if (packageFilter != null) { 161 val pkg = item.containingPackage(false) 162 if (pkg != null && !packageFilter.matches(pkg)) { 163 return false 164 } 165 } 166 } 167 168 val baseline = getBaseline() 169 if (item != null && baseline != null && baseline.mark(item, message, id)) { 170 return false 171 } else if (psi != null && baseline != null && baseline.mark(psi, message, id)) { 172 return false 173 } 174 175 return dispatch(this::doReport) 176 } 177 isSuppressednull178 fun isSuppressed(id: Issues.Issue, item: Item? = null, message: String? = null): Boolean { 179 val severity = configuration.getSeverity(id) 180 if (severity == HIDDEN) { 181 return true 182 } 183 184 item ?: return false 185 186 if (severity == LINT || severity == WARNING || severity == ERROR) { 187 val annotation = item.modifiers.findAnnotation("android.annotation.SuppressLint") 188 if (annotation != null) { 189 val attribute = annotation.findAttribute(ATTR_VALUE) 190 if (attribute != null) { 191 val id1 = "Doclava${id.code}" 192 val id2 = id.name 193 val value = attribute.value 194 if (value is AnnotationArrayAttributeValue) { 195 // Example: @SuppressLint({"DocLava1", "DocLava2"}) 196 for (innerValue in value.values) { 197 val string = innerValue.value()?.toString() ?: continue 198 if (suppressMatches(string, id1, message) || suppressMatches(string, id2, message)) { 199 return true 200 } 201 } 202 } else { 203 // Example: @SuppressLint("DocLava1") 204 val string = value.value()?.toString() 205 if (string != null && ( 206 suppressMatches(string, id1, message) || suppressMatches(string, id2, message)) 207 ) { 208 return true 209 } 210 } 211 } 212 } 213 } 214 215 return false 216 } 217 suppressMatchesnull218 private fun suppressMatches(value: String, id: String?, message: String?): Boolean { 219 id ?: return false 220 221 if (value == id) { 222 return true 223 } 224 225 if (message != null && value.startsWith(id) && value.endsWith(message) && 226 (value == "$id:$message" || value == "$id: $message") 227 ) { 228 return true 229 } 230 231 return false 232 } 233 getTextRangenull234 private fun getTextRange(element: PsiElement): TextRange? { 235 var range: TextRange? = null 236 237 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 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 = Math.min(offset, 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 in [report.dispatch] */ 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 if (effectiveSeverity == ERROR) { 314 hasErrors = true 315 errorCount++ 316 } else if (severity == WARNING) { 317 warningCount++ 318 } 319 320 reportPrinter( 321 format(effectiveSeverity, location, message, id, color, options.omitLocations), 322 effectiveSeverity 323 ) 324 return true 325 } 326 formatnull327 private fun format( 328 severity: Severity, 329 location: String?, 330 message: String, 331 id: Issues.Issue?, 332 color: Boolean, 333 omitLocations: Boolean 334 ): String { 335 val sb = StringBuilder(100) 336 337 if (color && !isUnderTest()) { 338 sb.append(terminalAttributes(bold = true)) 339 if (!omitLocations) { 340 location?.let { 341 sb.append(it).append(": ") 342 } 343 } 344 when (severity) { 345 LINT -> sb.append(terminalAttributes(foreground = TerminalColor.CYAN)).append("lint: ") 346 INFO -> sb.append(terminalAttributes(foreground = TerminalColor.CYAN)).append("info: ") 347 WARNING -> sb.append(terminalAttributes(foreground = TerminalColor.YELLOW)).append("warning: ") 348 ERROR -> sb.append(terminalAttributes(foreground = TerminalColor.RED)).append("error: ") 349 INHERIT, HIDDEN -> { 350 } 351 } 352 sb.append(resetTerminal()) 353 sb.append(message) 354 id?.let { 355 sb.append(" [").append(it.name).append("]") 356 } 357 } else { 358 if (!omitLocations) { 359 location?.let { sb.append(it).append(": ") } 360 } 361 if (compatibility.oldErrorOutputFormat) { 362 // according to doclava1 there are some people or tools parsing old format 363 when (severity) { 364 LINT -> sb.append("lint ") 365 INFO -> sb.append("info ") 366 WARNING -> sb.append("warning ") 367 ERROR -> sb.append("error ") 368 INHERIT, HIDDEN -> { 369 } 370 } 371 id?.let { sb.append(it.name).append(": ") } 372 sb.append(message) 373 } else { 374 when (severity) { 375 LINT -> sb.append("lint: ") 376 INFO -> sb.append("info: ") 377 WARNING -> sb.append("warning: ") 378 ERROR -> sb.append("error: ") 379 INHERIT, HIDDEN -> { 380 } 381 } 382 sb.append(message) 383 id?.let { 384 sb.append(" [") 385 sb.append(it.name) 386 if (compatibility.includeExitCode) { 387 sb.append(":") 388 sb.append(it.code) 389 } 390 sb.append("]") 391 if (it.rule != null) { 392 sb.append(" [Rule ").append(it.rule) 393 val link = it.category.ruleLink 394 if (link != null) { 395 sb.append(" in ").append(link) 396 } 397 sb.append("]") 398 } 399 } 400 } 401 } 402 return sb.toString() 403 } 404 reportEvenIfSuppressednull405 private fun reportEvenIfSuppressed( 406 severity: Severity, 407 location: String?, 408 message: String, 409 id: Issues.Issue 410 ): Boolean { 411 options.reportEvenIfSuppressedWriter?.println( 412 format( 413 severity, 414 location, 415 message, 416 id, 417 color = false, 418 omitLocations = false 419 )) 420 return true 421 } 422 hasErrorsnull423 fun hasErrors(): Boolean = hasErrors 424 425 /** Write the error message set to this [Reporter], if any errors have been detected. */ 426 fun writeErrorMessage(writer: PrintWriter) { 427 if (hasErrors()) { 428 errorMessage ?. let { writer.write(it) } 429 } 430 } 431 getBaselineDescriptionnull432 fun getBaselineDescription(): String { 433 val file = getBaseline()?.file 434 return if (file != null) { 435 "baseline ${file.path}" 436 } else { 437 "no baseline" 438 } 439 } 440 441 companion object { 442 /** root folder, which needs to be changed for unit tests. */ 443 @VisibleForTesting 444 internal var rootFolder: File? = File("").absoluteFile 445 446 /** Injection point for unit tests. */ severitynull447 internal var reportPrinter: (String, Severity) -> Unit = { message, severity -> 448 val output = if (severity == ERROR) { 449 options.stderr 450 } else { 451 options.stdout 452 } 453 output.println() 454 output.print(message.trim()) 455 output.flush() 456 } 457 } 458 } 459