1 /* 2 * 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.Errors 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.intellij.openapi.util.TextRange 33 import com.intellij.openapi.vfs.StandardFileSystems 34 import com.intellij.openapi.vfs.VfsUtilCore 35 import com.intellij.openapi.vfs.VirtualFile 36 import com.intellij.psi.PsiCompiledElement 37 import com.intellij.psi.PsiElement 38 import com.intellij.psi.PsiModifierListOwner 39 import com.intellij.psi.impl.light.LightElement 40 import java.io.File 41 42 var reporter = Reporter() 43 44 enum class Severity(private val displayName: String) { 45 INHERIT("inherit"), 46 47 HIDDEN("hidden"), 48 49 /** 50 * Information level are for issues that are informational only; may or 51 * may not be a problem. 52 */ 53 INFO("info"), 54 55 /** 56 * Lint level means that we encountered inconsistent or broken documentation. 57 * These should be resolved, but don't impact API compatibility. 58 */ 59 LINT("lint"), 60 61 /** 62 * Warning level means that we encountered some incompatible or inconsistent 63 * API change. These must be resolved to preserve API compatibility. 64 */ 65 WARNING("warning"), 66 67 /** 68 * Error level means that we encountered severe trouble and were unable to 69 * output the requested documentation. 70 */ 71 ERROR("error"); 72 toStringnull73 override fun toString(): String = displayName 74 } 75 76 open class Reporter(private val rootFolder: File? = null) { 77 var errorCount = 0 78 private set 79 var warningCount = 0 80 private set 81 val totalCount get() = errorCount + warningCount 82 83 private var hasErrors = false 84 85 fun report(id: Errors.Error, element: PsiElement?, message: String): Boolean { 86 val severity = configuration.getSeverity(id) 87 88 if (severity == HIDDEN) { 89 return false 90 } 91 92 val baseline = options.baseline 93 if (element != null && baseline != null && baseline.mark(element, message, id)) { 94 return false 95 } 96 97 return report(severity, element, message, id) 98 } 99 100 fun report(id: Errors.Error, file: File?, message: String): Boolean { 101 val severity = configuration.getSeverity(id) 102 103 if (severity == HIDDEN) { 104 return false 105 } 106 107 val baseline = options.baseline 108 if (file != null && baseline != null && baseline.mark(file, message, id)) { 109 return false 110 } 111 112 return report(severity, file?.path, message, id) 113 } 114 115 fun report(id: Errors.Error, item: Item?, message: String, psi: PsiElement? = null): Boolean { 116 if (isSuppressed(id, item, message)) { 117 return false 118 } 119 120 val severity = configuration.getSeverity(id) 121 122 if (severity == HIDDEN) { 123 return false 124 } 125 126 // If we are only emitting some packages (--stub-packages), don't report 127 // issues from other packages 128 if (item != null) { 129 val packageFilter = options.stubPackages 130 if (packageFilter != null) { 131 val pkg = item.containingPackage(false) 132 if (pkg != null && !packageFilter.matches(pkg)) { 133 return false 134 } 135 } 136 } 137 138 val baseline = options.baseline 139 if (item != null && baseline != null && baseline.mark(item, message, id)) { 140 return false 141 } else if (psi != null && baseline != null && baseline.mark(psi, message, id)) { 142 return false 143 } 144 145 return when { 146 psi != null -> { 147 report(severity, psi, message, id) 148 } 149 item is PsiItem -> { 150 report(severity, item.psi(), message, id) 151 } 152 item is TextItem -> report(severity, (item as? TextItem)?.position.toString(), message, id) 153 else -> report(severity, null as String?, message, id) 154 } 155 } 156 157 fun isSuppressed(id: Errors.Error, item: Item? = null, message: String? = null): Boolean { 158 val severity = configuration.getSeverity(id) 159 if (severity == HIDDEN) { 160 return true 161 } 162 163 item ?: return false 164 165 if (severity == LINT || severity == WARNING || severity == ERROR) { 166 val annotation = item.modifiers.findAnnotation("android.annotation.SuppressLint") 167 if (annotation != null) { 168 val attribute = annotation.findAttribute(ATTR_VALUE) 169 if (attribute != null) { 170 val id1 = "Doclava${id.code}" 171 val id2 = id.name 172 val value = attribute.value 173 if (value is AnnotationArrayAttributeValue) { 174 // Example: @SuppressLint({"DocLava1", "DocLava2"}) 175 for (innerValue in value.values) { 176 val string = innerValue.value()?.toString() ?: continue 177 if (suppressMatches(string, id1, message) || suppressMatches(string, id2, message)) { 178 return true 179 } 180 } 181 } else { 182 // Example: @SuppressLint("DocLava1") 183 val string = value.value()?.toString() 184 if (string != null && ( 185 suppressMatches(string, id1, message) || suppressMatches(string, id2, message)) 186 ) { 187 return true 188 } 189 } 190 } 191 } 192 } 193 194 return false 195 } 196 197 private fun suppressMatches(value: String, id: String?, message: String?): Boolean { 198 id ?: return false 199 200 if (value == id) { 201 return true 202 } 203 204 if (message != null && value.startsWith(id) && value.endsWith(message) && 205 (value == "$id:$message" || value == "$id: $message") 206 ) { 207 return true 208 } 209 210 return false 211 } 212 213 private fun getTextRange(element: PsiElement): TextRange? { 214 var range: TextRange? = null 215 216 if (element is PsiCompiledElement) { 217 if (element is LightElement) { 218 range = (element as PsiElement).textRange 219 } 220 if (range == null || TextRange.EMPTY_RANGE == range) { 221 return null 222 } 223 } else { 224 range = element.textRange 225 } 226 227 return range 228 } 229 230 fun elementToLocation(element: PsiElement?, includeDocs: Boolean = true): String? { 231 element ?: return null 232 val psiFile = element.containingFile ?: return null 233 val virtualFile = psiFile.virtualFile ?: return null 234 val file = VfsUtilCore.virtualToIoFile(virtualFile) 235 236 val path = 237 if (rootFolder != null) { 238 val root: VirtualFile? = StandardFileSystems.local().findFileByPath(rootFolder.path) 239 if (root != null) VfsUtilCore.getRelativePath(virtualFile, root) ?: file.path else file.path 240 } else { 241 file.path 242 } 243 244 // Skip doc comments for classes, methods and fields; we usually want to point right to 245 // the class/method/field definition 246 val rangeElement = if (!includeDocs && element is PsiModifierListOwner) { 247 element.modifierList ?: element 248 } else 249 element 250 251 val range = getTextRange(rangeElement) 252 val lineNumber = if (range == null) { 253 // No source offsets, use invalid line number 254 -1 255 } else { 256 getLineNumber(psiFile.text, range.startOffset) + 1 257 } 258 return if (lineNumber > 0) "$path:$lineNumber" else path 259 } 260 261 /** Returns the 0-based line number */ 262 private fun getLineNumber(text: String, offset: Int): Int { 263 var line = 0 264 var curr = 0 265 val target = Math.min(offset, text.length) 266 while (curr < target) { 267 if (text[curr++] == '\n') { 268 line++ 269 } 270 } 271 return line 272 } 273 274 private fun report(severity: Severity, element: PsiElement?, message: String, id: Errors.Error? = null): Boolean { 275 if (severity == HIDDEN) { 276 return false 277 } 278 279 return report(severity, elementToLocation(element), message, id) 280 } 281 282 open fun report( 283 severity: Severity, 284 location: String?, 285 message: String, 286 id: Errors.Error? = null, 287 color: Boolean = options.color 288 ): Boolean { 289 if (severity == HIDDEN) { 290 return false 291 } 292 293 val effectiveSeverity = 294 if (severity == LINT && options.lintsAreErrors) 295 ERROR 296 else if (severity == WARNING && options.warningsAreErrors) { 297 ERROR 298 } else { 299 severity 300 } 301 302 if (severity == ERROR) { 303 hasErrors = true 304 errorCount++ 305 } else if (severity == WARNING) { 306 warningCount++ 307 } 308 309 val sb = StringBuilder(100) 310 311 if (color && !isUnderTest()) { 312 sb.append(terminalAttributes(bold = true)) 313 if (!options.omitLocations) { 314 location?.let { 315 sb.append(it).append(": ") 316 } 317 } 318 when (effectiveSeverity) { 319 LINT -> sb.append(terminalAttributes(foreground = TerminalColor.CYAN)).append("lint: ") 320 INFO -> sb.append(terminalAttributes(foreground = TerminalColor.CYAN)).append("info: ") 321 WARNING -> sb.append(terminalAttributes(foreground = TerminalColor.YELLOW)).append("warning: ") 322 ERROR -> sb.append(terminalAttributes(foreground = TerminalColor.RED)).append("error: ") 323 INHERIT, HIDDEN -> { 324 } 325 } 326 sb.append(resetTerminal()) 327 sb.append(message) 328 id?.let { sb.append(" [").append(if (it.name != null) it.name else it.code).append("]") } 329 } else { 330 if (!options.omitLocations) { 331 location?.let { sb.append(it).append(": ") } 332 } 333 if (compatibility.oldErrorOutputFormat) { 334 // according to doclava1 there are some people or tools parsing old format 335 when (effectiveSeverity) { 336 LINT -> sb.append("lint ") 337 INFO -> sb.append("info ") 338 WARNING -> sb.append("warning ") 339 ERROR -> sb.append("error ") 340 INHERIT, HIDDEN -> { 341 } 342 } 343 id?.let { sb.append(if (it.name != null) it.name else it.code).append(": ") } 344 sb.append(message) 345 } else { 346 when (effectiveSeverity) { 347 LINT -> sb.append("lint: ") 348 INFO -> sb.append("info: ") 349 WARNING -> sb.append("warning: ") 350 ERROR -> sb.append("error: ") 351 INHERIT, HIDDEN -> { 352 } 353 } 354 sb.append(message) 355 id?.let { 356 sb.append(" [") 357 if (it.name != null) { 358 sb.append(it.name) 359 } 360 if (compatibility.includeExitCode || it.name == null) { 361 if (it.name != null) { 362 sb.append(":") 363 } 364 sb.append(it.code) 365 } 366 sb.append("]") 367 if (it.rule != null) { 368 sb.append(" [Rule ").append(it.rule) 369 val link = it.category.ruleLink 370 if (link != null) { 371 sb.append(" in ").append(link) 372 } 373 sb.append("]") 374 } 375 } 376 } 377 } 378 print(sb.toString()) 379 return true 380 } 381 382 open fun print(message: String) { 383 options.stdout.println() 384 options.stdout.print(message.trim()) 385 options.stdout.flush() 386 } 387 388 fun hasErrors(): Boolean = hasErrors 389 }