1 /* 2 * Copyright (C) 2024 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 com.android.tools.metalava.reporter.Severity.ERROR 20 import com.android.tools.metalava.reporter.Severity.HIDDEN 21 import com.android.tools.metalava.reporter.Severity.WARNING 22 import java.io.File 23 import java.io.OutputStreamWriter 24 import java.io.PrintWriter 25 import java.nio.file.Path 26 import java.util.function.Predicate 27 28 class DefaultReporter( 29 private val environment: ReporterEnvironment, 30 private val issueConfiguration: IssueConfiguration, 31 32 /** [Baseline] file associated with this [Reporter]. */ 33 private val baseline: Baseline? = null, 34 35 /** 36 * An error message associated with this [Reporter], which should be shown to the user when 37 * metalava finishes with errors. 38 */ 39 private val errorMessage: String? = null, 40 41 /** Filter to hide issues reported on specific types of [Reportable]. */ 42 private val reportableFilter: Predicate<Reportable>? = null, 43 44 /** Additional config properties. */ 45 private val config: Config = Config(), 46 ) : Reporter { 47 48 /** A list of [Report] objects containing all the reported issues. */ 49 private val reports = mutableListOf<Report>() 50 51 private var warningCount = 0 52 53 /** 54 * Configuration properties for the reporter. 55 * 56 * This contains properties that are shared across all instances of [DefaultReporter], except 57 * for the bootstrapping reporter. That receives a default instance of this. 58 */ 59 class Config( 60 /** If true, treat all warnings as errors */ 61 val warningsAsErrors: Boolean = false, 62 63 /** Formats the report suitable for use in a file. */ 64 val fileReportFormatter: ReportFormatter = DefaultReportFormatter.DEFAULT, 65 66 /** Formats the report for output, e.g. to a terminal. */ 67 val outputReportFormatter: ReportFormatter = fileReportFormatter, 68 69 /** 70 * Optional writer to which, if present, all errors, even if they were suppressed in 71 * baseline or via annotation, will be written. 72 */ 73 val reportEvenIfSuppressedWriter: PrintWriter? = null, 74 ) 75 76 /** The number of errors. */ 77 var errorCount: Int = 0 78 private set 79 80 /** Returns whether any errors have been detected. */ hasErrorsnull81 fun hasErrors(): Boolean = errorCount > 0 82 83 override fun report( 84 id: Issues.Issue, 85 reportable: Reportable?, 86 message: String, 87 location: FileLocation, 88 maximumSeverity: Severity, 89 ): Boolean { 90 val severity = issueConfiguration.getSeverity(id) 91 val upgradedSeverity = 92 if (severity == WARNING && config.warningsAsErrors) { 93 ERROR 94 } else { 95 severity 96 } 97 98 // Limit the Severity to the maximum allowed. 99 val effectiveSeverity = minOf(upgradedSeverity, maximumSeverity) 100 if (effectiveSeverity == HIDDEN) { 101 return false 102 } 103 104 // When selecting a location to use for reporting the issue the location is used in 105 // preference to the item because the location is more specific. e.g. if the item is a 106 // method then the location may be a line within the body of the method. 107 val reportLocation = 108 when { 109 location.path != null -> location 110 else -> reportable?.fileLocation 111 } 112 113 val report = 114 Report( 115 severity = effectiveSeverity, 116 // Relativize the path before storing in the Report. 117 relativePath = reportLocation?.path?.relativizeLocationPath(), 118 line = reportLocation?.line ?: 0, 119 message = message, 120 issue = id, 121 ) 122 123 // Optionally write to the --report-even-if-suppressed file. 124 reportEvenIfSuppressed(report) 125 126 if (isSuppressed(id, reportable, message)) { 127 return false 128 } 129 130 // Apply the reportable filter if one is provided. 131 if (reportable != null && reportableFilter?.test(reportable) == false) { 132 return false 133 } 134 135 if (baseline != null) { 136 // When selecting a key to use for in checking the baseline the reportable key is used 137 // in preference to the location because the reportable key is more stable. e.g. the 138 // location key may be for a specific line within a method which would change over time 139 // while a key based off a method's would stay the same. 140 val baselineKey = 141 when { 142 // When available use the baseline key from the reportable. 143 reportable != null -> reportable.baselineKey 144 // Otherwise, use the baseline key from the file location. 145 else -> location.baselineKey 146 } 147 148 if (baselineKey != null && baseline.mark(baselineKey, message, id)) return false 149 } 150 151 return doReport(report) 152 } 153 isSuppressednull154 override fun isSuppressed( 155 id: Issues.Issue, 156 reportable: Reportable?, 157 message: String? 158 ): Boolean { 159 val severity = issueConfiguration.getSeverity(id) 160 if (severity == HIDDEN) { 161 return true 162 } 163 164 reportable ?: return false 165 166 // Suppress the issue if requested for the item. 167 return reportable.suppressedIssues().any { suppressMatches(it, id.name, message) } 168 } 169 suppressMatchesnull170 private fun suppressMatches(value: String, id: String?, message: String?): Boolean { 171 id ?: return false 172 173 if (value == id) { 174 return true 175 } 176 177 if ( 178 message != null && 179 value.startsWith(id) && 180 value.endsWith(message) && 181 (value == "$id:$message" || value == "$id: $message") 182 ) { 183 return true 184 } 185 186 return false 187 } 188 189 /** 190 * Relativize this against the [ReporterEnvironment.rootFolder] if specified. 191 * 192 * Tests will set [ReporterEnvironment.rootFolder] to the temporary directory so that this can 193 * remove that from any paths that are reported to avoid the test having to be aware of the 194 * temporary directory. 195 */ Pathnull196 private fun Path.relativizeLocationPath(): String { 197 // b/255575766: Note that `relativize` requires two paths to compare to have same types: 198 // either both of them are absolute paths or both of them are not absolute paths. 199 val path = environment.rootFolder.toPath().relativize(this) ?: this 200 return path.toString() 201 } 202 203 /** Alias to allow method reference to `dispatch` in [report] */ doReportnull204 private fun doReport(report: Report): Boolean { 205 val severity = report.severity 206 when (severity) { 207 ERROR -> errorCount++ 208 WARNING -> warningCount++ 209 else -> {} 210 } 211 212 reports.add(report) 213 return true 214 } 215 reportEvenIfSuppressednull216 private fun reportEvenIfSuppressed(report: Report): Boolean { 217 config.reportEvenIfSuppressedWriter?.println(config.fileReportFormatter.format(report)) 218 return true 219 } 220 221 /** Print all the recorded errors to the given writer. Returns the number of errors printed. */ printErrorsnull222 fun printErrors(writer: PrintWriter, maxErrors: Int): Int { 223 val errors = reports.filter { it.severity == ERROR }.take(maxErrors) 224 for (error in errors) { 225 val formattedMessage = config.outputReportFormatter.format(error) 226 writer.println(formattedMessage) 227 } 228 return errors.size 229 } 230 231 /** Write all reports. */ writeSavedReportsnull232 fun writeSavedReports() { 233 // Sort the reports in place. This will ensure that the errors output in [printErrors] are 234 // also sorted in the same order as that is called after this. 235 reports.sortWith(reportComparator) 236 237 // Print out all the save reports. 238 for (report in reports) { 239 val formattedMessage = config.outputReportFormatter.format(report) 240 environment.printReport(formattedMessage, report.severity) 241 } 242 } 243 244 /** Write the error message set to this [Reporter], if any errors have been detected. */ writeErrorMessagenull245 fun writeErrorMessage(writer: PrintWriter) { 246 if (hasErrors()) { 247 errorMessage?.let { writer.write(it) } 248 } 249 } 250 251 companion object { 252 private val reportComparator = 253 compareBy<Report>( <lambda>null254 { it.relativePath }, <lambda>null255 { it.line }, <lambda>null256 { it.severity }, <lambda>null257 { it.issue?.name }, <lambda>null258 { it.message }, 259 ) 260 } 261 } 262 263 /** 264 * Provides access to information about the environment within which the [Reporter] will be being 265 * used. 266 */ 267 interface ReporterEnvironment { 268 269 /** Root folder, against which location paths will be relativized to simplify the output. */ 270 val rootFolder: File 271 272 /** Print the report. */ printReportnull273 fun printReport(message: String, severity: Severity) 274 } 275 276 class DefaultReporterEnvironment( 277 val stdout: PrintWriter = PrintWriter(OutputStreamWriter(System.out)), 278 val stderr: PrintWriter = PrintWriter(OutputStreamWriter(System.err)), 279 ) : ReporterEnvironment { 280 281 override val rootFolder = File("").absoluteFile 282 283 override fun printReport(message: String, severity: Severity) { 284 val output = if (severity == ERROR) stderr else stdout 285 output.println(message.trim()) 286 output.flush() 287 } 288 } 289