• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }