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 )