• 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.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.Issues
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.google.common.annotations.VisibleForTesting
33 import com.intellij.openapi.util.TextRange
34 import com.intellij.openapi.vfs.VfsUtilCore
35 import com.intellij.psi.PsiCompiledElement
36 import com.intellij.psi.PsiElement
37 import com.intellij.psi.PsiModifierListOwner
38 import com.intellij.psi.impl.light.LightElement
39 import java.io.File
40 import java.io.PrintWriter
41 
42 /**
43  * "Global" [Reporter] used by most operations.
44  * Certain operations, such as api-lint and compatibility check, may use a custom [Reporter]
45  */
46 lateinit var reporter: Reporter
47 
48 enum class Severity(private val displayName: String) {
49     INHERIT("inherit"),
50 
51     HIDDEN("hidden"),
52 
53     /**
54      * Information level are for issues that are informational only; may or
55      * may not be a problem.
56      */
57     INFO("info"),
58 
59     /**
60      * Lint level means that we encountered inconsistent or broken documentation.
61      * These should be resolved, but don't impact API compatibility.
62      */
63     LINT("lint"),
64 
65     /**
66      * Warning level means that we encountered some incompatible or inconsistent
67      * API change. These must be resolved to preserve API compatibility.
68      */
69     WARNING("warning"),
70 
71     /**
72      * Error level means that we encountered severe trouble and were unable to
73      * output the requested documentation.
74      */
75     ERROR("error");
76 
77     override fun toString(): String = displayName
78 }
79 
80 class Reporter(
81     /** [Baseline] file associated with this [Reporter]. If null, the global baseline is used. */
82     // See the comment on [getBaseline] for why it's nullable.
83     private val customBaseline: Baseline?,
84 
85     /**
86      * An error message associated with this [Reporter], which should be shown to the user
87      * when metalava finishes with errors.
88      */
89     private val errorMessage: String?
90 ) {
91     var errorCount = 0
92         private set
93     var warningCount = 0
94         private set
95     val totalCount get() = errorCount + warningCount
96 
97     private var hasErrors = false
98 
99     // Note we can't set [options.baseline] as the default for [customBaseline], because
100     // options.baseline will be initialized after the global [Reporter] is instantiated.
getBaselinenull101     fun getBaseline(): Baseline? = customBaseline ?: options.baseline
102 
103     fun report(id: Issues.Issue, element: PsiElement?, message: String): Boolean {
104         val severity = configuration.getSeverity(id)
105 
106         if (severity == HIDDEN) {
107             return false
108         }
109 
110         val baseline = getBaseline()
111         if (element != null && baseline != null && baseline.mark(element, message, id)) {
112             return false
113         }
114 
115         return report(severity, elementToLocation(element), message, id)
116     }
117 
reportnull118     fun report(id: Issues.Issue, file: File?, message: String): Boolean {
119         val severity = configuration.getSeverity(id)
120 
121         if (severity == HIDDEN) {
122             return false
123         }
124 
125         val baseline = getBaseline()
126         if (file != null && baseline != null && baseline.mark(file, message, id)) {
127             return false
128         }
129 
130         return report(severity, file?.path, message, id)
131     }
132 
reportnull133     fun report(id: Issues.Issue, item: Item?, message: String, psi: PsiElement? = null): Boolean {
134         val severity = configuration.getSeverity(id)
135         if (severity == HIDDEN) {
136             return false
137         }
138 
139         fun dispatch(
140             which: (severity: Severity, location: String?, message: String, id: Issues.Issue) -> Boolean
141         ) = when {
142             psi != null -> which(severity, elementToLocation(psi), message, id)
143             item is PsiItem -> which(severity, elementToLocation(item.psi()), message, id)
144             item is TextItem ->
145                 which(severity, (item as? TextItem)?.position.toString(), message, id)
146             else -> which(severity, null as String?, message, id)
147         }
148 
149         // Optionally write to the --report-even-if-suppressed file.
150         dispatch(this::reportEvenIfSuppressed)
151 
152         if (isSuppressed(id, item, message)) {
153             return false
154         }
155 
156         // If we are only emitting some packages (--stub-packages), don't report
157         // issues from other packages
158         if (item != null) {
159             val packageFilter = options.stubPackages
160             if (packageFilter != null) {
161                 val pkg = item.containingPackage(false)
162                 if (pkg != null && !packageFilter.matches(pkg)) {
163                     return false
164                 }
165             }
166         }
167 
168         val baseline = getBaseline()
169         if (item != null && baseline != null && baseline.mark(item, message, id)) {
170             return false
171         } else if (psi != null && baseline != null && baseline.mark(psi, message, id)) {
172             return false
173         }
174 
175         return dispatch(this::doReport)
176     }
177 
isSuppressednull178     fun isSuppressed(id: Issues.Issue, item: Item? = null, message: String? = null): Boolean {
179         val severity = configuration.getSeverity(id)
180         if (severity == HIDDEN) {
181             return true
182         }
183 
184         item ?: return false
185 
186         if (severity == LINT || severity == WARNING || severity == ERROR) {
187             val annotation = item.modifiers.findAnnotation("android.annotation.SuppressLint")
188             if (annotation != null) {
189                 val attribute = annotation.findAttribute(ATTR_VALUE)
190                 if (attribute != null) {
191                     val id1 = "Doclava${id.code}"
192                     val id2 = id.name
193                     val value = attribute.value
194                     if (value is AnnotationArrayAttributeValue) {
195                         // Example: @SuppressLint({"DocLava1", "DocLava2"})
196                         for (innerValue in value.values) {
197                             val string = innerValue.value()?.toString() ?: continue
198                             if (suppressMatches(string, id1, message) || suppressMatches(string, id2, message)) {
199                                 return true
200                             }
201                         }
202                     } else {
203                         // Example: @SuppressLint("DocLava1")
204                         val string = value.value()?.toString()
205                         if (string != null && (
206                                 suppressMatches(string, id1, message) || suppressMatches(string, id2, message))
207                         ) {
208                             return true
209                         }
210                     }
211                 }
212             }
213         }
214 
215         return false
216     }
217 
suppressMatchesnull218     private fun suppressMatches(value: String, id: String?, message: String?): Boolean {
219         id ?: return false
220 
221         if (value == id) {
222             return true
223         }
224 
225         if (message != null && value.startsWith(id) && value.endsWith(message) &&
226             (value == "$id:$message" || value == "$id: $message")
227         ) {
228             return true
229         }
230 
231         return false
232     }
233 
getTextRangenull234     private fun getTextRange(element: PsiElement): TextRange? {
235         var range: TextRange? = null
236 
237         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     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 = Math.min(offset, 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 in [report.dispatch] */
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         if (effectiveSeverity == ERROR) {
314             hasErrors = true
315             errorCount++
316         } else if (severity == WARNING) {
317             warningCount++
318         }
319 
320         reportPrinter(
321             format(effectiveSeverity, location, message, id, color, options.omitLocations),
322             effectiveSeverity
323         )
324         return true
325     }
326 
formatnull327     private fun format(
328         severity: Severity,
329         location: String?,
330         message: String,
331         id: Issues.Issue?,
332         color: Boolean,
333         omitLocations: Boolean
334     ): String {
335         val sb = StringBuilder(100)
336 
337         if (color && !isUnderTest()) {
338             sb.append(terminalAttributes(bold = true))
339             if (!omitLocations) {
340                 location?.let {
341                     sb.append(it).append(": ")
342                 }
343             }
344             when (severity) {
345                 LINT -> sb.append(terminalAttributes(foreground = TerminalColor.CYAN)).append("lint: ")
346                 INFO -> sb.append(terminalAttributes(foreground = TerminalColor.CYAN)).append("info: ")
347                 WARNING -> sb.append(terminalAttributes(foreground = TerminalColor.YELLOW)).append("warning: ")
348                 ERROR -> sb.append(terminalAttributes(foreground = TerminalColor.RED)).append("error: ")
349                 INHERIT, HIDDEN -> {
350                 }
351             }
352             sb.append(resetTerminal())
353             sb.append(message)
354             id?.let {
355                 sb.append(" [").append(it.name).append("]")
356             }
357         } else {
358             if (!omitLocations) {
359                 location?.let { sb.append(it).append(": ") }
360             }
361             if (compatibility.oldErrorOutputFormat) {
362                 // according to doclava1 there are some people or tools parsing old format
363                 when (severity) {
364                     LINT -> sb.append("lint ")
365                     INFO -> sb.append("info ")
366                     WARNING -> sb.append("warning ")
367                     ERROR -> sb.append("error ")
368                     INHERIT, HIDDEN -> {
369                     }
370                 }
371                 id?.let { sb.append(it.name).append(": ") }
372                 sb.append(message)
373             } else {
374                 when (severity) {
375                     LINT -> sb.append("lint: ")
376                     INFO -> sb.append("info: ")
377                     WARNING -> sb.append("warning: ")
378                     ERROR -> sb.append("error: ")
379                     INHERIT, HIDDEN -> {
380                     }
381                 }
382                 sb.append(message)
383                 id?.let {
384                     sb.append(" [")
385                     sb.append(it.name)
386                     if (compatibility.includeExitCode) {
387                         sb.append(":")
388                         sb.append(it.code)
389                     }
390                     sb.append("]")
391                     if (it.rule != null) {
392                         sb.append(" [Rule ").append(it.rule)
393                         val link = it.category.ruleLink
394                         if (link != null) {
395                             sb.append(" in ").append(link)
396                         }
397                         sb.append("]")
398                     }
399                 }
400             }
401         }
402         return sb.toString()
403     }
404 
reportEvenIfSuppressednull405     private fun reportEvenIfSuppressed(
406         severity: Severity,
407         location: String?,
408         message: String,
409         id: Issues.Issue
410     ): Boolean {
411         options.reportEvenIfSuppressedWriter?.println(
412             format(
413                 severity,
414                 location,
415                 message,
416                 id,
417                 color = false,
418                 omitLocations = false
419             ))
420         return true
421     }
422 
hasErrorsnull423     fun hasErrors(): Boolean = hasErrors
424 
425     /** Write the error message set to this [Reporter], if any errors have been detected. */
426     fun writeErrorMessage(writer: PrintWriter) {
427         if (hasErrors()) {
428             errorMessage ?. let { writer.write(it) }
429         }
430     }
431 
getBaselineDescriptionnull432     fun getBaselineDescription(): String {
433         val file = getBaseline()?.file
434         return if (file != null) {
435             "baseline ${file.path}"
436         } else {
437             "no baseline"
438         }
439     }
440 
441     companion object {
442         /** root folder, which needs to be changed for unit tests. */
443         @VisibleForTesting
444         internal var rootFolder: File? = File("").absoluteFile
445 
446         /** Injection point for unit tests. */
severitynull447         internal var reportPrinter: (String, Severity) -> Unit = { message, severity ->
448             val output = if (severity == ERROR) {
449                 options.stderr
450             } else {
451                 options.stdout
452             }
453             output.println()
454             output.print(message.trim())
455             output.flush()
456         }
457     }
458 }
459