• 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.PsiNameIdentifierOwner
37 import com.intellij.psi.impl.light.LightElement
38 import org.jetbrains.kotlin.psi.KtModifierListOwner
39 import org.jetbrains.uast.UClass
40 import org.jetbrains.uast.UElement
41 import java.io.File
42 import java.io.PrintWriter
43 
44 /**
45  * "Global" [Reporter] used by most operations.
46  * Certain operations, such as api-lint and compatibility check, may use a custom [Reporter]
47  */
48 lateinit var reporter: Reporter
49 
50 enum class Severity(private val displayName: String) {
51     INHERIT("inherit"),
52 
53     HIDDEN("hidden"),
54 
55     /**
56      * Information level are for issues that are informational only; may or
57      * may not be a problem.
58      */
59     INFO("info"),
60 
61     /**
62      * Lint level means that we encountered inconsistent or broken documentation.
63      * These should be resolved, but don't impact API compatibility.
64      */
65     LINT("lint"),
66 
67     /**
68      * Warning level means that we encountered some incompatible or inconsistent
69      * API change. These must be resolved to preserve API compatibility.
70      */
71     WARNING("warning"),
72 
73     /**
74      * Error level means that we encountered severe trouble and were unable to
75      * output the requested documentation.
76      */
77     ERROR("error");
78 
79     override fun toString(): String = displayName
80 }
81 
82 class Reporter(
83     /** [Baseline] file associated with this [Reporter]. If null, the global baseline is used. */
84     // See the comment on [getBaseline] for why it's nullable.
85     private val customBaseline: Baseline?,
86 
87     /**
88      * An error message associated with this [Reporter], which should be shown to the user
89      * when metalava finishes with errors.
90      */
91     private val errorMessage: String?
92 ) {
93     private var errors = mutableListOf<String>()
94     private var warningCount = 0
95     val totalCount get() = errors.size + warningCount
96 
97     /** The number of errors. */
98     val errorCount get() = errors.size
99 
100     /** Returns whether any errors have been detected. */
hasErrorsnull101     fun hasErrors(): Boolean = errors.size > 0
102 
103     // Note we can't set [options.baseline] as the default for [customBaseline], because
104     // options.baseline will be initialized after the global [Reporter] is instantiated.
105     private fun getBaseline(): Baseline? = customBaseline ?: options.baseline
106 
107     fun report(id: Issues.Issue, element: PsiElement?, message: String): Boolean {
108         val severity = configuration.getSeverity(id)
109 
110         if (severity == HIDDEN) {
111             return false
112         }
113 
114         val baseline = getBaseline()
115         if (element != null && baseline != null && baseline.mark(element, message, id)) {
116             return false
117         }
118 
119         return report(severity, elementToLocation(element), message, id)
120     }
121 
reportnull122     fun report(id: Issues.Issue, file: File?, message: String): Boolean {
123         val severity = configuration.getSeverity(id)
124 
125         if (severity == HIDDEN) {
126             return false
127         }
128 
129         val baseline = getBaseline()
130         if (file != null && baseline != null && baseline.mark(file, message, id)) {
131             return false
132         }
133 
134         return report(severity, file?.path, message, id)
135     }
136 
reportnull137     fun report(id: Issues.Issue, item: Item?, message: String, psi: PsiElement? = null): Boolean {
138         val severity = configuration.getSeverity(id)
139         if (severity == HIDDEN) {
140             return false
141         }
142 
143         fun dispatch(
144             which: (severity: Severity, location: String?, message: String, id: Issues.Issue) -> Boolean
145         ) = when {
146             psi != null -> which(severity, elementToLocation(psi), message, id)
147             item is PsiItem -> which(severity, elementToLocation(item.psi()), message, id)
148             item is TextItem ->
149                 which(severity, (item as? TextItem)?.position.toString(), message, id)
150             else -> which(severity, null as String?, message, id)
151         }
152 
153         // Optionally write to the --report-even-if-suppressed file.
154         dispatch(this::reportEvenIfSuppressed)
155 
156         if (isSuppressed(id, item, message)) {
157             return false
158         }
159 
160         // If we are only emitting some packages (--stub-packages), don't report
161         // issues from other packages
162         if (item != null) {
163             val packageFilter = options.stubPackages
164             if (packageFilter != null) {
165                 val pkg = item.containingPackage(false)
166                 if (pkg != null && !packageFilter.matches(pkg)) {
167                     return false
168                 }
169             }
170         }
171 
172         val baseline = getBaseline()
173         if (item != null && baseline != null && baseline.mark(item, message, id)) {
174             return false
175         } else if (psi != null && baseline != null && baseline.mark(psi, message, id)) {
176             return false
177         }
178 
179         return dispatch(this::doReport)
180     }
181 
isSuppressednull182     fun isSuppressed(id: Issues.Issue, item: Item? = null, message: String? = null): Boolean {
183         val severity = configuration.getSeverity(id)
184         if (severity == HIDDEN) {
185             return true
186         }
187 
188         item ?: return false
189 
190         for (annotation in item.modifiers.annotations()) {
191             val annotationName = annotation.qualifiedName
192             if (annotationName != null && annotationName in SUPPRESS_ANNOTATIONS) {
193                 for (attribute in annotation.attributes) {
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({"RequiresFeature", "AllUpper"})
199                         for (innerValue in value.values) {
200                             val string = innerValue.value()?.toString() ?: continue
201                             if (suppressMatches(string, id.name, message)) {
202                                 return true
203                             }
204                         }
205                     } else {
206                         // Example: @SuppressLint("RequiresFeature")
207                         val string = value.value()?.toString()
208                         if (string != null && (suppressMatches(string, id.name, message))) {
209                             return true
210                         }
211                     }
212                 }
213             }
214         }
215 
216         return false
217     }
218 
suppressMatchesnull219     private fun suppressMatches(value: String, id: String?, message: String?): Boolean {
220         id ?: return false
221 
222         if (value == id) {
223             return true
224         }
225 
226         if (message != null && value.startsWith(id) && value.endsWith(message) &&
227             (value == "$id:$message" || value == "$id: $message")
228         ) {
229             return true
230         }
231 
232         return false
233     }
234 
getTextRangenull235     private fun getTextRange(element: PsiElement): TextRange? {
236         var range: TextRange? = null
237 
238         if (element is UClass) {
239             range = element.sourcePsi?.textRange
240         } else if (element is PsiCompiledElement) {
241             if (element is LightElement) {
242                 range = (element as PsiElement).textRange
243             }
244             if (range == null || TextRange.EMPTY_RANGE == range) {
245                 return null
246             }
247         } else {
248             range = element.textRange
249         }
250 
251         return range
252     }
253 
elementToLocationnull254     private fun elementToLocation(element: PsiElement?): String? {
255         element ?: return null
256         val psiFile = element.containingFile ?: return null
257         val virtualFile = psiFile.virtualFile ?: return null
258         val file = VfsUtilCore.virtualToIoFile(virtualFile)
259 
260         val path = (rootFolder?.toPath()?.relativize(file.toPath()) ?: file.toPath()).toString()
261 
262         // Unwrap UAST for accurate Kotlin line numbers (UAST synthesizes text offsets sometimes)
263         val sourceElement = (element as? UElement)?.sourcePsi ?: element
264 
265         // Skip doc comments for classes, methods and fields by pointing at the line where the
266         // element's name is or falling back to the first line of its modifier list (which may
267         // include annotations) or lastly to the start of the element itself
268         val rangeElement = (sourceElement as? PsiNameIdentifierOwner)?.nameIdentifier
269             ?: (sourceElement as? KtModifierListOwner)?.modifierList
270             ?: (sourceElement as? PsiModifierListOwner)?.modifierList
271             ?: sourceElement
272 
273         val range = getTextRange(rangeElement)
274         val lineNumber = if (range == null) {
275             -1 // No source offsets, use invalid line number
276         } else {
277             getLineNumber(psiFile.text, range.startOffset) + 1
278         }
279         return if (lineNumber > 0) "$path:$lineNumber" else path
280     }
281 
282     /** Returns the 0-based line number of character position <offset> in <text> */
getLineNumbernull283     private fun getLineNumber(text: String, offset: Int): Int {
284         var line = 0
285         var curr = 0
286         val target = offset.coerceAtMost(text.length)
287         while (curr < target) {
288             if (text[curr++] == '\n') {
289                 line++
290             }
291         }
292         return line
293     }
294 
295     /** Alias to allow method reference to `dispatch` in [report] */
doReportnull296     private fun doReport(severity: Severity, location: String?, message: String, id: Issues.Issue?) =
297         report(severity, location, message, id)
298 
299     fun report(
300         severity: Severity,
301         location: String?,
302         message: String,
303         id: Issues.Issue? = null,
304         color: Boolean = options.color
305     ): Boolean {
306         if (severity == HIDDEN) {
307             return false
308         }
309 
310         val effectiveSeverity =
311             if (severity == LINT && options.lintsAreErrors)
312                 ERROR
313             else if (severity == WARNING && options.warningsAreErrors) {
314                 ERROR
315             } else {
316                 severity
317             }
318 
319         val formattedMessage = format(effectiveSeverity, location, message, id, color, options.omitLocations)
320         if (effectiveSeverity == ERROR) {
321             errors.add(formattedMessage)
322         } else if (severity == WARNING) {
323             warningCount++
324         }
325 
326         reportPrinter(formattedMessage, effectiveSeverity)
327         return true
328     }
329 
formatnull330     private fun format(
331         severity: Severity,
332         location: String?,
333         message: String,
334         id: Issues.Issue?,
335         color: Boolean,
336         omitLocations: Boolean
337     ): String {
338         val sb = StringBuilder(100)
339 
340         if (color && !isUnderTest()) {
341             sb.append(terminalAttributes(bold = true))
342             if (!omitLocations) {
343                 location?.let {
344                     sb.append(it).append(": ")
345                 }
346             }
347             when (severity) {
348                 LINT -> sb.append(terminalAttributes(foreground = TerminalColor.CYAN)).append("lint: ")
349                 INFO -> sb.append(terminalAttributes(foreground = TerminalColor.CYAN)).append("info: ")
350                 WARNING -> sb.append(terminalAttributes(foreground = TerminalColor.YELLOW)).append("warning: ")
351                 ERROR -> sb.append(terminalAttributes(foreground = TerminalColor.RED)).append("error: ")
352                 INHERIT, HIDDEN -> {
353                 }
354             }
355             sb.append(resetTerminal())
356             sb.append(message)
357             id?.let {
358                 sb.append(" [").append(it.name).append("]")
359             }
360         } else {
361             if (!omitLocations) {
362                 location?.let { sb.append(it).append(": ") }
363             }
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             sb.append(message)
373             id?.let {
374                 sb.append(" [")
375                 sb.append(it.name)
376                 sb.append("]")
377                 val link = it.category.ruleLink
378                 if (it.rule != null && link != null) {
379                     sb.append(" [See ").append(link).append(it.rule)
380                     sb.append("]")
381                 }
382             }
383         }
384         return sb.toString()
385     }
386 
reportEvenIfSuppressednull387     private fun reportEvenIfSuppressed(
388         severity: Severity,
389         location: String?,
390         message: String,
391         id: Issues.Issue
392     ): Boolean {
393         options.reportEvenIfSuppressedWriter?.println(
394             format(
395                 severity,
396                 location,
397                 message,
398                 id,
399                 color = false,
400                 omitLocations = false
401             )
402         )
403         return true
404     }
405 
406     /**
407      * Print all the recorded errors to the given writer. Returns the number of errors printer.
408      */
printErrorsnull409     fun printErrors(writer: PrintWriter, maxErrors: Int): Int {
410         var i = 0
411         errors.forEach loop@{
412             if (i >= maxErrors) {
413                 return@loop
414             }
415             i++
416             writer.println(it)
417         }
418         return i
419     }
420 
421     /** Write the error message set to this [Reporter], if any errors have been detected. */
writeErrorMessagenull422     fun writeErrorMessage(writer: PrintWriter) {
423         if (hasErrors()) {
424             errorMessage ?. let { writer.write(it) }
425         }
426     }
427 
getBaselineDescriptionnull428     fun getBaselineDescription(): String {
429         val file = getBaseline()?.file
430         return if (file != null) {
431             "baseline ${file.path}"
432         } else {
433             "no baseline"
434         }
435     }
436 
437     companion object {
438         /** root folder, which needs to be changed for unit tests. */
439         @VisibleForTesting
440         internal var rootFolder: File? = File("").absoluteFile
441 
442         /** Injection point for unit tests. */
severitynull443         internal var reportPrinter: (String, Severity) -> Unit = { message, severity ->
444             val output = if (severity == ERROR) {
445                 options.stderr
446             } else {
447                 options.stdout
448             }
449             output.println()
450             output.print(message.trim())
451             output.flush()
452         }
453     }
454 }
455 
456 private val SUPPRESS_ANNOTATIONS = listOf(
457     ANDROID_SUPPRESS_LINT,
458     JAVA_LANG_SUPPRESS_WARNINGS,
459     KOTLIN_SUPPRESS
460 )
461