1 /* 2 * Copyright (C) 2023 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.reporter 18 19 import java.io.File 20 import java.io.OutputStream 21 import java.io.PrintWriter 22 import java.io.StringWriter 23 import java.io.Writer 24 25 interface Reporter { 26 27 /** 28 * Report an issue with a specific file. 29 * 30 * Delegates to calling `report(id, null, message, Location.forFile(file)`. 31 * 32 * @param id the id of the issue. 33 * @param file the optional source file for which the issue is reported. 34 * @param message the message to report. 35 * @param maximumSeverity the maximum [Severity] that will be reported. An issue that is 36 * configured to have a higher [Severity] that this will use the [maximumSeverity] instead. 37 * @return true if the issue was reported false it is a known issue in a baseline file. 38 */ reportnull39 fun report( 40 id: Issues.Issue, 41 file: File?, 42 message: String, 43 maximumSeverity: Severity = Severity.UNLIMITED, 44 ): Boolean { 45 val location = FileLocation.forFile(file) 46 return report(id, null, message, location, maximumSeverity) 47 } 48 49 /** 50 * Report an issue. 51 * 52 * The issue is handled as follows: 53 * 1. If the item is suppressed (see [isSuppressed]) then it will only be reported to a file 54 * into which suppressed issues are reported and this will return `false`. 55 * 2. If possible the issue will be checked in a relevant baseline file to see if it is a known 56 * issue and if so it will simply be ignored. 57 * 3. Otherwise, it will be reported at the appropriate severity to the command output and if 58 * possible it will be recorded in a new baseline file that the developer can copy to silence 59 * the issue in the future. 60 * 61 * If no [location] or [reportable] is provided then no location is reported in the error 62 * message, and the baseline file is neither checked nor updated. 63 * 64 * If a [location] is provided but no [reportable] then it is used both to report the message 65 * and as the baseline key to check and update the baseline file. 66 * 67 * If an [reportable] is provided but no [location] then it is used both to report the message 68 * and as the baseline key to check and update the baseline file. 69 * 70 * If both an [reportable] and [location] are provided then the [reportable] is used as the 71 * baseline key to check and update the baseline file and the [location] is used to report the 72 * message. The reason for that is the [location] is assumed to be a more accurate indication of 73 * where the problem lies but the [reportable] is assumed to provide a more stable key to use in 74 * the baseline as it will not change simply by adding and removing lines in the containing 75 * file. 76 * 77 * @param id the id of the issue. 78 * @param reportable the optional object for which the issue is reported. 79 * @param message the message to report. 80 * @param location the optional location to specify. 81 * @param maximumSeverity the maximum [Severity] that will be reported. An issue that is 82 * configured to have a higher [Severity] that this will use the [maximumSeverity] instead. 83 * @return true if the issue was reported false it is a known issue in a baseline file. 84 */ reportnull85 fun report( 86 id: Issues.Issue, 87 reportable: Reportable?, 88 message: String, 89 location: FileLocation = FileLocation.UNKNOWN, 90 maximumSeverity: Severity = Severity.UNLIMITED, 91 ): Boolean 92 93 /** 94 * Check to see whether the issue is suppressed. 95 * 1. If the [Severity] of the [Issues.Issue] is [Severity.HIDDEN] then this returns `true`. 96 * 2. If the [reportable] is `null` then this returns `false`. 97 * 3. If the item has a suppression annotation that lists the name of the issue then this 98 * returns `true`. 99 * 4. Otherwise, this returns `false`. 100 */ 101 fun isSuppressed( 102 id: Issues.Issue, 103 reportable: Reportable? = null, 104 message: String? = null 105 ): Boolean 106 } 107 108 /** 109 * Abstract implementation of a [Reporter] that performs no filtering and delegates the handling of 110 * a report to [handleFormattedMessage]. 111 */ 112 abstract class AbstractBasicReporter : Reporter { 113 override fun report( 114 id: Issues.Issue, 115 reportable: Reportable?, 116 message: String, 117 location: FileLocation, 118 maximumSeverity: Severity, 119 ): Boolean { 120 val formattedMessage = buildString { 121 val usableLocation = reportable?.fileLocation ?: location 122 append(usableLocation.path) 123 if (usableLocation.line > 0) { 124 append(":") 125 append(usableLocation.line) 126 } 127 append(": ") 128 val severity = id.defaultLevel 129 append(severity) 130 append(": ") 131 append(message) 132 append(severity.messageSuffix) 133 append(" [") 134 append(id.name) 135 append("]") 136 } 137 return handleFormattedMessage(formattedMessage) 138 } 139 140 abstract fun handleFormattedMessage(formattedMessage: String): Boolean 141 142 override fun isSuppressed( 143 id: Issues.Issue, 144 reportable: Reportable?, 145 message: String? 146 ): Boolean = false 147 } 148 149 /** 150 * Basic implementation of a [Reporter] that performs no filtering and simply outputs the message to 151 * the supplied [PrintWriter]. 152 */ 153 class BasicReporter(private val stderr: PrintWriter) : AbstractBasicReporter() { 154 constructor(writer: Writer) : this(stderr = PrintWriter(writer)) 155 156 constructor(outputStream: OutputStream) : this(stderr = PrintWriter(outputStream)) 157 handleFormattedMessagenull158 override fun handleFormattedMessage(formattedMessage: String): Boolean { 159 stderr.println(formattedMessage) 160 stderr.flush() 161 return true 162 } 163 isSuppressednull164 override fun isSuppressed( 165 id: Issues.Issue, 166 reportable: Reportable?, 167 message: String? 168 ): Boolean = false 169 } 170 171 /** A [Reporter] which will record issues in an internal buffer, accessible through [issues]. */ 172 class RecordingReporter : AbstractBasicReporter() { 173 private val stringWriter = StringWriter() 174 175 override fun handleFormattedMessage(formattedMessage: String): Boolean { 176 stringWriter.append(formattedMessage).append("\n") 177 return true 178 } 179 180 val issues: String 181 get() = stringWriter.toString().trim() 182 183 /** Remove and return any existing issues. */ 184 fun removeIssues(): String { 185 val issues = stringWriter.toString().trim() 186 stringWriter.buffer.setLength(0) 187 return issues 188 } 189 } 190 191 /** 192 * A [Reporter] which will throw an exception for the first issue, even warnings or hidden, that is 193 * reported. 194 * 195 * Safe to use when no issues are expected as it will prevent any issues from being silently 196 * ignored. 197 */ 198 class ThrowingReporter private constructor() : AbstractBasicReporter() { handleFormattedMessagenull199 override fun handleFormattedMessage(formattedMessage: String): Boolean { 200 error(formattedMessage) 201 } 202 203 companion object { 204 val INSTANCE = ThrowingReporter() 205 } 206 } 207