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.INHERIT 23 import com.android.tools.metalava.Severity.LINT 24 import com.android.tools.metalava.Severity.WARNING 25 import com.android.tools.metalava.doclava1.Errors 26 import com.android.tools.metalava.model.AnnotationArrayAttributeValue 27 import com.android.tools.metalava.model.Item 28 import com.android.tools.metalava.model.psi.PsiConstructorItem 29 import com.android.tools.metalava.model.psi.PsiItem 30 import com.android.tools.metalava.model.text.TextItem 31 import com.intellij.openapi.util.TextRange 32 import com.intellij.openapi.vfs.StandardFileSystems 33 import com.intellij.openapi.vfs.VfsUtilCore 34 import com.intellij.openapi.vfs.VirtualFile 35 import com.intellij.psi.PsiCompiledElement 36 import com.intellij.psi.PsiElement 37 import com.intellij.psi.impl.light.LightElement 38 import java.io.File 39 40 var reporter = Reporter() 41 42 enum class Severity(private val displayName: String) { 43 INHERIT("inherit"), 44 45 HIDDEN("hidden"), 46 47 /** 48 * Lint level means that we encountered inconsistent or broken documentation. 49 * These should be resolved, but don't impact API compatibility. 50 */ 51 LINT("lint"), 52 53 /** 54 * Warning level means that we encountered some incompatible or inconsistent 55 * API change. These must be resolved to preserve API compatibility. 56 */ 57 WARNING("warning"), 58 59 /** 60 * Error level means that we encountered severe trouble and were unable to 61 * output the requested documentation. 62 */ 63 ERROR("error"); 64 65 override fun toString(): String = displayName 66 } 67 68 open class Reporter(private val rootFolder: File? = null) { 69 var hasErrors = false 70 71 fun error(item: Item?, message: String, id: Errors.Error? = null) { 72 error(item?.psi(), message, id) 73 } 74 75 fun warning(item: Item?, message: String, id: Errors.Error? = null) { 76 warning(item?.psi(), message, id) 77 } 78 79 fun error(element: PsiElement?, message: String, id: Errors.Error? = null) { 80 // Using lowercase since that's the convention doclava1 is using 81 report(ERROR, element, message, id) 82 } 83 84 fun warning(element: PsiElement?, message: String, id: Errors.Error? = null) { 85 report(WARNING, element, message, id) 86 } 87 88 fun report(id: Errors.Error, element: PsiElement?, message: String) { 89 report(id.level, element, message, id) 90 } 91 92 fun report(id: Errors.Error, file: File?, message: String) { 93 report(id.level, file?.path, message, id) 94 } 95 96 fun report(id: Errors.Error, item: Item?, message: String) { 97 if (isSuppressed(id, item)) { 98 return 99 } 100 101 when (item) { 102 is PsiItem -> { 103 var psi = item.psi() 104 105 // If no PSI element, is this a synthetic/implicit constructor? If so 106 // grab the parent class' PSI element instead for file/location purposes 107 108 if (item is PsiConstructorItem && item.implicitConstructor && 109 psi?.containingFile?.virtualFile == null 110 ) { 111 psi = item.containingClass().psi() 112 } 113 114 report(id.level, psi, message, id) 115 } 116 is TextItem -> report(id.level, (item as? TextItem)?.position.toString(), message, id) 117 else -> report(id.level, "<unknown location>", message, id) 118 } 119 } 120 121 private fun isSuppressed(id: Errors.Error, item: Item?): Boolean { 122 item ?: return false 123 124 if (id.level == LINT || id.level == WARNING || id.level == ERROR) { 125 val id1 = "Doclava${id.code}" 126 val id2 = id.name 127 val annotation = item.modifiers.findAnnotation("android.annotation.SuppressLint") 128 if (annotation != null) { 129 val attribute = annotation.findAttribute(ATTR_VALUE) 130 if (attribute != null) { 131 val value = attribute.value 132 if (value is AnnotationArrayAttributeValue) { 133 // Example: @SuppressLint({"DocLava1", "DocLava2"}) 134 for (innerValue in value.values) { 135 val string = innerValue.value() 136 if (id1 == string || id2 != null && id2 == string) { 137 return true 138 } 139 } 140 } else { 141 // Example: @SuppressLint("DocLava1") 142 val string = value.value() 143 if (id1 == string || id2 != null && id2 == string) { 144 return true 145 } 146 } 147 } 148 } 149 } 150 151 return false 152 } 153 154 private fun getTextRange(element: PsiElement): TextRange? { 155 var range: TextRange? = null 156 157 if (element is PsiCompiledElement) { 158 if (element is LightElement) { 159 range = (element as PsiElement).textRange 160 } 161 if (range == null || TextRange.EMPTY_RANGE == range) { 162 return null 163 } 164 } else { 165 range = element.textRange 166 } 167 168 return range 169 } 170 171 private fun elementToLocation(element: PsiElement?): String? { 172 element ?: return null 173 val psiFile = element.containingFile ?: return null 174 val virtualFile = psiFile.virtualFile ?: return null 175 val file = VfsUtilCore.virtualToIoFile(virtualFile) 176 177 val path = 178 if (rootFolder != null) { 179 val root: VirtualFile? = StandardFileSystems.local().findFileByPath(rootFolder.path) 180 if (root != null) VfsUtilCore.getRelativePath(virtualFile, root) ?: file.path else file.path 181 } else { 182 file.path 183 } 184 185 val range = getTextRange(element) 186 return if (range == null) { 187 // No source offsets, just use filename 188 path 189 } else { 190 val lineNumber = getLineNumber(psiFile.text, range.startOffset) + 1 191 "$path:$lineNumber" 192 } 193 } 194 195 /** Returns the 0-based line number */ 196 private fun getLineNumber(text: String, offset: Int): Int { 197 var line = 0 198 var curr = 0 199 val target = Math.min(offset, text.length) 200 while (curr < target) { 201 if (text[curr++] == '\n') { 202 line++ 203 } 204 } 205 return line 206 } 207 208 open fun report(severity: Severity, element: PsiElement?, message: String, id: Errors.Error? = null) { 209 if (severity == HIDDEN) { 210 return 211 } 212 213 report(severity, elementToLocation(element), message, id) 214 } 215 216 open fun report( 217 severity: Severity, 218 location: String?, 219 message: String, 220 id: Errors.Error? = null, 221 color: Boolean = options.color 222 ) { 223 if (severity == HIDDEN) { 224 return 225 } 226 227 val effectiveSeverity = 228 if (severity == LINT && options.lintsAreErrors) 229 ERROR 230 else if (severity == WARNING && options.warningsAreErrors) { 231 ERROR 232 } else { 233 severity 234 } 235 236 if (severity == ERROR) { 237 hasErrors = true 238 } 239 240 val sb = StringBuilder(100) 241 242 if (color) { 243 sb.append(terminalAttributes(bold = true)) 244 if (!options.omitLocations) { 245 location?.let { sb.append(it).append(": ") } 246 } 247 when (effectiveSeverity) { 248 LINT -> sb.append(terminalAttributes(foreground = TerminalColor.CYAN)).append("lint: ") 249 WARNING -> sb.append(terminalAttributes(foreground = TerminalColor.YELLOW)).append("warning: ") 250 ERROR -> sb.append(terminalAttributes(foreground = TerminalColor.RED)).append("error: ") 251 INHERIT, HIDDEN -> { 252 } 253 } 254 sb.append(resetTerminal()) 255 sb.append(message) 256 id?.let { sb.append(" [").append(if (it.name != null) it.name else it.code).append("]") } 257 } else { 258 if (!options.omitLocations) { 259 location?.let { sb.append(it).append(": ") } 260 } 261 if (compatibility.oldErrorOutputFormat) { 262 // according to doclava1 there are some people or tools parsing old format 263 when (effectiveSeverity) { 264 LINT -> sb.append("lint ") 265 WARNING -> sb.append("warning ") 266 ERROR -> sb.append("error ") 267 INHERIT, HIDDEN -> { 268 } 269 } 270 id?.let { sb.append(if (it.name != null) it.name else it.code).append(": ") } 271 sb.append(message) 272 } else { 273 when (effectiveSeverity) { 274 LINT -> sb.append("lint: ") 275 WARNING -> sb.append("warning: ") 276 ERROR -> sb.append("error: ") 277 INHERIT, HIDDEN -> { 278 } 279 } 280 sb.append(message) 281 id?.let { 282 sb.append(" [") 283 if (it.name != null) { 284 sb.append(it.name).append(":") 285 } 286 sb.append(it.code) 287 sb.append("]") 288 } 289 } 290 } 291 print(sb.toString()) 292 } 293 294 open fun print(message: String) { 295 options.stdout.println() 296 options.stdout.print(message.trim()) 297 options.stdout.flush() 298 } 299 300 fun hasErrors(): Boolean = hasErrors 301 }