blob: 342836dd314f76bd883d17c93eac4ef67b0e3036 [file] [log] [blame]
/*
* Copyright (C) 2011 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.lint
import com.android.tools.lint.client.api.LintClient
import com.android.tools.lint.detector.api.LintFix
import com.android.tools.lint.detector.api.LintFix.GroupType.ALTERNATIVES
import com.android.tools.lint.detector.api.LintFix.LintFixGroup
import com.android.tools.lint.detector.api.Location
import com.android.tools.lint.detector.api.TextFormat.RAW
import com.android.utils.SdkUtils
import com.android.utils.XmlUtils.toXmlAttributeValue
import com.google.common.annotations.Beta
import com.google.common.base.Joiner
import com.google.common.io.Files
import java.io.BufferedWriter
import java.io.File
import java.io.IOException
import java.io.Writer
import kotlin.math.max
import kotlin.math.min
/**
* A reporter which emits lint results into an XML report.
*
*
* **NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.**
*/
@Beta
class XmlReporter
/**
* Constructs a new [XmlReporter]
*
* @param client the client
*
* @param output the output file
*
* @throws IOException if an error occurs
*/
@Throws(IOException::class)
constructor(client: LintCliClient, output: File) : Reporter(client, output) {
private val writer: Writer = BufferedWriter(Files.newWriter(output, Charsets.UTF_8))
var isIntendedForBaseline: Boolean = false
var includeFixes: Boolean = false
private var attributes: MutableMap<String, String>? = null
/** Sets a custom attribute to be written out on the root element of the report */
private fun setAttribute(name: String, value: String) {
val attributes = attributes ?: run {
val newMap = mutableMapOf<String, String>()
attributes = newMap
newMap
}
attributes[name] = value
}
fun setBaselineAttributes(client: LintClient, variant: String?) {
setAttribute("client", LintClient.clientName)
val revision = client.getClientDisplayRevision()
if (revision != null) {
setAttribute("version", revision)
}
if (variant != null) {
setAttribute("variant", variant)
}
}
@Throws(IOException::class)
override fun write(stats: LintStats, issues: List<Warning>) {
writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
// Format 4: added urls= attribute with all more info links, comma separated
writer.write("<issues format=\"5\"")
val revision = client.getClientDisplayRevision()
if (revision != null) {
writer.write(String.format(" by=\"lint %1\$s\"", revision))
}
attributes?.let { map ->
map.asSequence().sortedBy { it.key }.forEach {
writer.write(" ${it.key}=\"${toXmlAttributeValue(it.value)}\"")
}
}
writer.write(">\n")
if (issues.isNotEmpty()) {
writeIssues(issues)
}
writer.write("\n</issues>\n")
writer.close()
if (!client.flags.isQuiet && (stats.errorCount > 0 || stats.warningCount > 0)) {
val url = SdkUtils.fileToUrlString(output.absoluteFile)
println(String.format("Wrote XML report to %1\$s", url))
}
}
private fun writeIssues(issues: List<Warning>) {
for (warning in issues) {
writeIssue(warning)
}
}
private fun writeIssue(warning: Warning) {
writer.write('\n'.toInt())
indent(1)
writer.write("<issue")
val issue = warning.issue
writeAttribute(writer, 2, "id", issue.id)
if (!isIntendedForBaseline) {
writeAttribute(
writer, 2, "severity",
warning.severity.description
)
}
writeAttribute(writer, 2, "message", warning.message)
if (!isIntendedForBaseline) {
writeAttribute(writer, 2, "category", issue.category.fullName)
writeAttribute(writer, 2, "priority", issue.priority.toString())
// We don't need issue metadata in baselines
writeAttribute(writer, 2, "summary", issue.getBriefDescription(RAW))
writeAttribute(writer, 2, "explanation", issue.getExplanation(RAW))
val moreInfo = issue.moreInfo
if (moreInfo.isNotEmpty()) {
// Compatibility with old format: list first URL
writeAttribute(writer, 2, "url", moreInfo[0])
writeAttribute(writer, 2, "urls", Joiner.on(',').join(issue.moreInfo))
}
}
if (warning.errorLine != null && warning.errorLine.isNotEmpty()) {
val line = warning.errorLine
val index1 = line.indexOf('\n')
if (index1 != -1) {
val index2 = line.indexOf('\n', index1 + 1)
if (index2 != -1) {
val line1 = line.substring(0, index1)
val line2 = line.substring(index1 + 1, index2)
writeAttribute(writer, 2, "errorLine1", line1)
writeAttribute(writer, 2, "errorLine2", line2)
}
}
}
if (warning.isVariantSpecific) {
writeAttribute(
writer,
2,
"includedVariants",
Joiner.on(',').join(warning.includedVariantNames)
)
writeAttribute(
writer,
2,
"excludedVariants",
Joiner.on(',').join(warning.excludedVariantNames)
)
}
if (!isIntendedForBaseline && includeFixes &&
(warning.quickfixData != null || hasAutoFix(issue))
) {
writeAttribute(writer, 2, "quickfix", "studio")
}
assert(warning.file != null == (warning.location != null))
if (warning.file != null) {
assert(warning.location.file === warning.file)
}
var hasChildren = false
val fixData = warning.quickfixData
if (includeFixes && fixData != null) {
writer.write(">\n")
emitFixes(warning, fixData)
hasChildren = true
}
var location: Location? = warning.location
if (location != null) {
if (!hasChildren) {
writer.write(">\n")
}
while (location != null) {
indent(2)
writer.write("<location")
val path = client.getDisplayPath(
warning.project,
location.file,
// Don't use absolute paths in baseline files
client.flags.isFullPath && !isIntendedForBaseline
)
writeAttribute(writer, 3, "file", path)
val start = location.start
if (start != null) {
val line = start.line
val column = start.column
if (line >= 0) {
// +1: Line numbers internally are 0-based, report should be
// 1-based.
writeAttribute(writer, 3, "line", (line + 1).toString())
if (column >= 0) {
writeAttribute(writer, 3, "column", (column + 1).toString())
}
}
}
writer.write("/>\n")
location = location.secondary
}
hasChildren = true
}
if (hasChildren) {
indent(1)
writer.write("</issue>\n")
} else {
writer.write('\n'.toInt())
indent(1)
writer.write("/>\n")
}
}
private fun emitFixes(warning: Warning, lintFix: LintFix) {
val fixes = if (lintFix is LintFixGroup && lintFix.type == ALTERNATIVES) {
lintFix.fixes
} else {
listOf(lintFix)
}
for (fix in fixes) {
emitFix(warning, fix)
}
}
private fun emitFix(warning: Warning, lintFix: LintFix) {
indent(2)
writer.write("<fix")
lintFix.displayName?.let {
writeAttribute(writer, 3, "description", it)
}
writeAttribute(
writer, 3, "auto",
LintFixPerformer.canAutoFix(lintFix).toString()
)
var haveChildren = false
val performer = LintFixPerformer(client, false)
val files = performer.computeEdits(warning, lintFix)
if (files != null && files.isNotEmpty()) {
haveChildren = true
writer.write(">\n")
for (file in files) {
for (edit in file.edits) {
indent(3)
writer.write("<edit")
val path = client.getDisplayPath(
warning.project,
file.file,
// Don't use absolute paths in baseline files
client.flags.isFullPath && !isIntendedForBaseline
)
writeAttribute(writer, 4, "file", path)
with(edit) {
val after = source.substring(max(startOffset - 12, 0), startOffset)
val before = source.substring(
startOffset,
min(max(startOffset + 12, endOffset), source.length)
)
writeAttribute(writer, 4, "offset", startOffset.toString())
writeAttribute(writer, 4, "after", after)
writeAttribute(writer, 4, "before", before)
if (endOffset > startOffset) {
writeAttribute(
writer, 4, "delete",
source.substring(startOffset, endOffset)
)
}
if (replacement.isNotEmpty()) {
writeAttribute(writer, 4, "insert", replacement)
}
}
writer.write("/>\n")
}
}
}
if (haveChildren) {
indent(2)
writer.write("</fix>\n")
} else {
writer.write("/>\n")
}
}
@Throws(IOException::class)
private fun writeAttribute(writer: Writer, indent: Int, name: String, value: String) {
writer.write('\n'.toInt())
indent(indent)
writer.write(name)
writer.write('='.toInt())
writer.write('"'.toInt())
writer.write(toXmlAttributeValue(value))
writer.write('"'.toInt())
}
@Throws(IOException::class)
private fun indent(indent: Int) {
for (level in 0 until indent) {
writer.write(" ")
}
}
}