• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.model.AnnotationArrayAttributeValue
26 import com.android.tools.metalava.model.Item
27 import com.android.tools.metalava.model.configuration
28 import com.android.tools.metalava.model.psi.PsiItem
29 import com.android.tools.metalava.model.text.TextItem
30 import com.google.common.annotations.VisibleForTesting
31 import com.intellij.openapi.util.TextRange
32 import com.intellij.openapi.vfs.VfsUtilCore
33 import com.intellij.psi.PsiCompiledElement
34 import com.intellij.psi.PsiElement
35 import com.intellij.psi.PsiModifierListOwner
36 import com.intellij.psi.impl.light.LightElement
37 import org.jetbrains.uast.kotlin.KotlinUClass
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         for (annotation in item.modifiers.annotations()) {
188             val annotationName = annotation.qualifiedName()
189             if (annotationName != null && annotationName in SUPPRESS_ANNOTATIONS) {
190                 for (attribute in annotation.attributes()) {
191                     // Assumption that all annotations in SUPPRESS_ANNOTATIONS only have
192                     // one attribute such as value/names that is varargs of String
193                     val value = attribute.value
194                     if (value is AnnotationArrayAttributeValue) {
195                         // Example: @SuppressLint({"RequiresFeature", "AllUpper"})
196                         for (innerValue in value.values) {
197                             val string = innerValue.value()?.toString() ?: continue
198                             if (suppressMatches(string, id.name, message)) {
199                                 return true
200                             }
201                         }
202                     } else {
203                         // Example: @SuppressLint("RequiresFeature")
204                         val string = value.value()?.toString()
205                         if (string != null && (suppressMatches(string, id.name, message))) {
206                             return true
207                         }
208                     }
209                 }
210             }
211         }
212 
213         return false
214     }
215 
suppressMatchesnull216     private fun suppressMatches(value: String, id: String?, message: String?): Boolean {
217         id ?: return false
218 
219         if (value == id) {
220             return true
221         }
222 
223         if (message != null && value.startsWith(id) && value.endsWith(message) &&
224             (value == "$id:$message" || value == "$id: $message")
225         ) {
226             return true
227         }
228 
229         return false
230     }
231 
getTextRangenull232     private fun getTextRange(element: PsiElement): TextRange? {
233         var range: TextRange? = null
234 
235         if (element is KotlinUClass) {
236             range = element.sourcePsi?.textRange
237         } else 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     private 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 = offset.coerceAtMost(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 to `dispatch` in [report] */
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         val formattedMessage = format(effectiveSeverity, location, message, id, color, options.omitLocations)
314         if (effectiveSeverity == ERROR) {
315             errors.add(formattedMessage)
316         } else if (severity == WARNING) {
317             warningCount++
318         }
319 
320         reportPrinter(formattedMessage, effectiveSeverity)
321         return true
322     }
323 
formatnull324     private fun format(
325         severity: Severity,
326         location: String?,
327         message: String,
328         id: Issues.Issue?,
329         color: Boolean,
330         omitLocations: Boolean
331     ): String {
332         val sb = StringBuilder(100)
333 
334         if (color && !isUnderTest()) {
335             sb.append(terminalAttributes(bold = true))
336             if (!omitLocations) {
337                 location?.let {
338                     sb.append(it).append(": ")
339                 }
340             }
341             when (severity) {
342                 LINT -> sb.append(terminalAttributes(foreground = TerminalColor.CYAN)).append("lint: ")
343                 INFO -> sb.append(terminalAttributes(foreground = TerminalColor.CYAN)).append("info: ")
344                 WARNING -> sb.append(terminalAttributes(foreground = TerminalColor.YELLOW)).append("warning: ")
345                 ERROR -> sb.append(terminalAttributes(foreground = TerminalColor.RED)).append("error: ")
346                 INHERIT, HIDDEN -> {
347                 }
348             }
349             sb.append(resetTerminal())
350             sb.append(message)
351             id?.let {
352                 sb.append(" [").append(it.name).append("]")
353             }
354         } else {
355             if (!omitLocations) {
356                 location?.let { sb.append(it).append(": ") }
357             }
358             when (severity) {
359                 LINT -> sb.append("lint: ")
360                 INFO -> sb.append("info: ")
361                 WARNING -> sb.append("warning: ")
362                 ERROR -> sb.append("error: ")
363                 INHERIT, HIDDEN -> {
364                 }
365             }
366             sb.append(message)
367             id?.let {
368                 sb.append(" [")
369                 sb.append(it.name)
370                 sb.append("]")
371                 val link = it.category.ruleLink
372                 if (it.rule != null && link != null) {
373                     sb.append(" [See ").append(link).append(it.rule)
374                     sb.append("]")
375                 }
376             }
377         }
378         return sb.toString()
379     }
380 
reportEvenIfSuppressednull381     private fun reportEvenIfSuppressed(
382         severity: Severity,
383         location: String?,
384         message: String,
385         id: Issues.Issue
386     ): Boolean {
387         options.reportEvenIfSuppressedWriter?.println(
388             format(
389                 severity,
390                 location,
391                 message,
392                 id,
393                 color = false,
394                 omitLocations = false
395             ))
396         return true
397     }
398 
399     /**
400      * Print all the recorded errors to the given writer. Returns the number of errors printer.
401      */
printErrorsnull402     fun printErrors(writer: PrintWriter, maxErrors: Int): Int {
403         var i = 0
404         errors.forEach loop@{
405             if (i >= maxErrors) {
406                 return@loop
407             }
408             i++
409             writer.println(it)
410         }
411         return i
412     }
413 
414     /** Write the error message set to this [Reporter], if any errors have been detected. */
writeErrorMessagenull415     fun writeErrorMessage(writer: PrintWriter) {
416         if (hasErrors()) {
417             errorMessage ?. let { writer.write(it) }
418         }
419     }
420 
getBaselineDescriptionnull421     fun getBaselineDescription(): String {
422         val file = getBaseline()?.file
423         return if (file != null) {
424             "baseline ${file.path}"
425         } else {
426             "no baseline"
427         }
428     }
429 
430     companion object {
431         /** root folder, which needs to be changed for unit tests. */
432         @VisibleForTesting
433         internal var rootFolder: File? = File("").absoluteFile
434 
435         /** Injection point for unit tests. */
severitynull436         internal var reportPrinter: (String, Severity) -> Unit = { message, severity ->
437             val output = if (severity == ERROR) {
438                 options.stderr
439             } else {
440                 options.stdout
441             }
442             output.println()
443             output.print(message.trim())
444             output.flush()
445         }
446     }
447 }
448 
449 private val SUPPRESS_ANNOTATIONS = listOf(
450     ANDROID_SUPPRESS_LINT,
451     JAVA_LANG_SUPPRESS_WARNINGS,
452     KOTLIN_SUPPRESS
453 )