blob: b830c61dc4bdd8e65f9ef108673b9e6c81d95f67 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.metalava
import com.android.tools.metalava.Severity.ERROR
import com.android.tools.metalava.Severity.HIDDEN
import com.android.tools.metalava.Severity.INFO
import com.android.tools.metalava.Severity.INHERIT
import com.android.tools.metalava.Severity.LINT
import com.android.tools.metalava.Severity.WARNING
import com.android.tools.metalava.model.AnnotationArrayAttributeValue
import com.android.tools.metalava.model.Item
import com.android.tools.metalava.model.configuration
import com.android.tools.metalava.model.psi.PsiItem
import com.android.tools.metalava.model.text.TextItem
import com.google.common.annotations.VisibleForTesting
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.psi.PsiCompiledElement
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiModifierListOwner
import com.intellij.psi.PsiNameIdentifierOwner
import com.intellij.psi.impl.light.LightElement
import org.jetbrains.kotlin.psi.KtModifierListOwner
import org.jetbrains.uast.UElement
import org.jetbrains.uast.kotlin.KotlinUClass
import java.io.File
import java.io.PrintWriter
/**
* "Global" [Reporter] used by most operations.
* Certain operations, such as api-lint and compatibility check, may use a custom [Reporter]
*/
lateinit var reporter: Reporter
enum class Severity(private val displayName: String) {
INHERIT("inherit"),
HIDDEN("hidden"),
/**
* Information level are for issues that are informational only; may or
* may not be a problem.
*/
INFO("info"),
/**
* Lint level means that we encountered inconsistent or broken documentation.
* These should be resolved, but don't impact API compatibility.
*/
LINT("lint"),
/**
* Warning level means that we encountered some incompatible or inconsistent
* API change. These must be resolved to preserve API compatibility.
*/
WARNING("warning"),
/**
* Error level means that we encountered severe trouble and were unable to
* output the requested documentation.
*/
ERROR("error");
override fun toString(): String = displayName
}
class Reporter(
/** [Baseline] file associated with this [Reporter]. If null, the global baseline is used. */
// See the comment on [getBaseline] for why it's nullable.
private val customBaseline: Baseline?,
/**
* An error message associated with this [Reporter], which should be shown to the user
* when metalava finishes with errors.
*/
private val errorMessage: String?
) {
private var errors = mutableListOf<String>()
private var warningCount = 0
val totalCount get() = errors.size + warningCount
/** The number of errors. */
val errorCount get() = errors.size
/** Returns whether any errors have been detected. */
fun hasErrors(): Boolean = errors.size > 0
// Note we can't set [options.baseline] as the default for [customBaseline], because
// options.baseline will be initialized after the global [Reporter] is instantiated.
private fun getBaseline(): Baseline? = customBaseline ?: options.baseline
fun report(id: Issues.Issue, element: PsiElement?, message: String): Boolean {
val severity = configuration.getSeverity(id)
if (severity == HIDDEN) {
return false
}
val baseline = getBaseline()
if (element != null && baseline != null && baseline.mark(element, message, id)) {
return false
}
return report(severity, elementToLocation(element), message, id)
}
fun report(id: Issues.Issue, file: File?, message: String): Boolean {
val severity = configuration.getSeverity(id)
if (severity == HIDDEN) {
return false
}
val baseline = getBaseline()
if (file != null && baseline != null && baseline.mark(file, message, id)) {
return false
}
return report(severity, file?.path, message, id)
}
fun report(id: Issues.Issue, item: Item?, message: String, psi: PsiElement? = null): Boolean {
val severity = configuration.getSeverity(id)
if (severity == HIDDEN) {
return false
}
fun dispatch(
which: (severity: Severity, location: String?, message: String, id: Issues.Issue) -> Boolean
) = when {
psi != null -> which(severity, elementToLocation(psi), message, id)
item is PsiItem -> which(severity, elementToLocation(item.psi()), message, id)
item is TextItem ->
which(severity, (item as? TextItem)?.position.toString(), message, id)
else -> which(severity, null as String?, message, id)
}
// Optionally write to the --report-even-if-suppressed file.
dispatch(this::reportEvenIfSuppressed)
if (isSuppressed(id, item, message)) {
return false
}
// If we are only emitting some packages (--stub-packages), don't report
// issues from other packages
if (item != null) {
val packageFilter = options.stubPackages
if (packageFilter != null) {
val pkg = item.containingPackage(false)
if (pkg != null && !packageFilter.matches(pkg)) {
return false
}
}
}
val baseline = getBaseline()
if (item != null && baseline != null && baseline.mark(item, message, id)) {
return false
} else if (psi != null && baseline != null && baseline.mark(psi, message, id)) {
return false
}
return dispatch(this::doReport)
}
fun isSuppressed(id: Issues.Issue, item: Item? = null, message: String? = null): Boolean {
val severity = configuration.getSeverity(id)
if (severity == HIDDEN) {
return true
}
item ?: return false
for (annotation in item.modifiers.annotations()) {
val annotationName = annotation.qualifiedName
if (annotationName != null && annotationName in SUPPRESS_ANNOTATIONS) {
for (attribute in annotation.attributes) {
// Assumption that all annotations in SUPPRESS_ANNOTATIONS only have
// one attribute such as value/names that is varargs of String
val value = attribute.value
if (value is AnnotationArrayAttributeValue) {
// Example: @SuppressLint({"RequiresFeature", "AllUpper"})
for (innerValue in value.values) {
val string = innerValue.value()?.toString() ?: continue
if (suppressMatches(string, id.name, message)) {
return true
}
}
} else {
// Example: @SuppressLint("RequiresFeature")
val string = value.value()?.toString()
if (string != null && (suppressMatches(string, id.name, message))) {
return true
}
}
}
}
}
return false
}
private fun suppressMatches(value: String, id: String?, message: String?): Boolean {
id ?: return false
if (value == id) {
return true
}
if (message != null && value.startsWith(id) && value.endsWith(message) &&
(value == "$id:$message" || value == "$id: $message")
) {
return true
}
return false
}
private fun getTextRange(element: PsiElement): TextRange? {
var range: TextRange? = null
if (element is KotlinUClass) {
range = element.sourcePsi?.textRange
} else if (element is PsiCompiledElement) {
if (element is LightElement) {
range = (element as PsiElement).textRange
}
if (range == null || TextRange.EMPTY_RANGE == range) {
return null
}
} else {
range = element.textRange
}
return range
}
private fun elementToLocation(element: PsiElement?): String? {
element ?: return null
val psiFile = element.containingFile ?: return null
val virtualFile = psiFile.virtualFile ?: return null
val file = VfsUtilCore.virtualToIoFile(virtualFile)
val path = (rootFolder?.toPath()?.relativize(file.toPath()) ?: file.toPath()).toString()
// Unwrap UAST for accurate Kotlin line numbers (UAST synthesizes text offsets sometimes)
val sourceElement = (element as? UElement)?.sourcePsi ?: element
// Skip doc comments for classes, methods and fields by pointing at the line where the
// element's name is or falling back to the first line of its modifier list (which may
// include annotations) or lastly to the start of the element itself
val rangeElement = (sourceElement as? PsiNameIdentifierOwner)?.nameIdentifier
?: (sourceElement as? KtModifierListOwner)?.modifierList
?: (sourceElement as? PsiModifierListOwner)?.modifierList
?: sourceElement
val range = getTextRange(rangeElement)
val lineNumber = if (range == null) {
-1 // No source offsets, use invalid line number
} else {
getLineNumber(psiFile.text, range.startOffset) + 1
}
return if (lineNumber > 0) "$path:$lineNumber" else path
}
/** Returns the 0-based line number of character position <offset> in <text> */
private fun getLineNumber(text: String, offset: Int): Int {
var line = 0
var curr = 0
val target = offset.coerceAtMost(text.length)
while (curr < target) {
if (text[curr++] == '\n') {
line++
}
}
return line
}
/** Alias to allow method reference to `dispatch` in [report] */
private fun doReport(severity: Severity, location: String?, message: String, id: Issues.Issue?) =
report(severity, location, message, id)
fun report(
severity: Severity,
location: String?,
message: String,
id: Issues.Issue? = null,
color: Boolean = options.color
): Boolean {
if (severity == HIDDEN) {
return false
}
val effectiveSeverity =
if (severity == LINT && options.lintsAreErrors)
ERROR
else if (severity == WARNING && options.warningsAreErrors) {
ERROR
} else {
severity
}
val formattedMessage = format(effectiveSeverity, location, message, id, color, options.omitLocations)
if (effectiveSeverity == ERROR) {
errors.add(formattedMessage)
} else if (severity == WARNING) {
warningCount++
}
reportPrinter(formattedMessage, effectiveSeverity)
return true
}
private fun format(
severity: Severity,
location: String?,
message: String,
id: Issues.Issue?,
color: Boolean,
omitLocations: Boolean
): String {
val sb = StringBuilder(100)
if (color && !isUnderTest()) {
sb.append(terminalAttributes(bold = true))
if (!omitLocations) {
location?.let {
sb.append(it).append(": ")
}
}
when (severity) {
LINT -> sb.append(terminalAttributes(foreground = TerminalColor.CYAN)).append("lint: ")
INFO -> sb.append(terminalAttributes(foreground = TerminalColor.CYAN)).append("info: ")
WARNING -> sb.append(terminalAttributes(foreground = TerminalColor.YELLOW)).append("warning: ")
ERROR -> sb.append(terminalAttributes(foreground = TerminalColor.RED)).append("error: ")
INHERIT, HIDDEN -> {
}
}
sb.append(resetTerminal())
sb.append(message)
id?.let {
sb.append(" [").append(it.name).append("]")
}
} else {
if (!omitLocations) {
location?.let { sb.append(it).append(": ") }
}
when (severity) {
LINT -> sb.append("lint: ")
INFO -> sb.append("info: ")
WARNING -> sb.append("warning: ")
ERROR -> sb.append("error: ")
INHERIT, HIDDEN -> {
}
}
sb.append(message)
id?.let {
sb.append(" [")
sb.append(it.name)
sb.append("]")
val link = it.category.ruleLink
if (it.rule != null && link != null) {
sb.append(" [See ").append(link).append(it.rule)
sb.append("]")
}
}
}
return sb.toString()
}
private fun reportEvenIfSuppressed(
severity: Severity,
location: String?,
message: String,
id: Issues.Issue
): Boolean {
options.reportEvenIfSuppressedWriter?.println(
format(
severity,
location,
message,
id,
color = false,
omitLocations = false
)
)
return true
}
/**
* Print all the recorded errors to the given writer. Returns the number of errors printer.
*/
fun printErrors(writer: PrintWriter, maxErrors: Int): Int {
var i = 0
errors.forEach loop@{
if (i >= maxErrors) {
return@loop
}
i++
writer.println(it)
}
return i
}
/** Write the error message set to this [Reporter], if any errors have been detected. */
fun writeErrorMessage(writer: PrintWriter) {
if (hasErrors()) {
errorMessage ?. let { writer.write(it) }
}
}
fun getBaselineDescription(): String {
val file = getBaseline()?.file
return if (file != null) {
"baseline ${file.path}"
} else {
"no baseline"
}
}
companion object {
/** root folder, which needs to be changed for unit tests. */
@VisibleForTesting
internal var rootFolder: File? = File("").absoluteFile
/** Injection point for unit tests. */
internal var reportPrinter: (String, Severity) -> Unit = { message, severity ->
val output = if (severity == ERROR) {
options.stderr
} else {
options.stdout
}
output.println()
output.print(message.trim())
output.flush()
}
}
}
private val SUPPRESS_ANNOTATIONS = listOf(
ANDROID_SUPPRESS_LINT,
JAVA_LANG_SUPPRESS_WARNINGS,
KOTLIN_SUPPRESS
)