1 /* 2 * 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.tools.metalava.cli.common.Terminal 20 import com.android.tools.metalava.cli.common.TerminalColor 21 import com.android.tools.metalava.cli.common.plainTerminal 22 import com.android.tools.metalava.model.Item 23 import com.android.tools.metalava.model.PackageItem 24 import com.android.tools.metalava.reporter.Baseline 25 import com.android.tools.metalava.reporter.FileLocation 26 import com.android.tools.metalava.reporter.IssueConfiguration 27 import com.android.tools.metalava.reporter.Issues 28 import com.android.tools.metalava.reporter.Reportable 29 import com.android.tools.metalava.reporter.Reporter 30 import com.android.tools.metalava.reporter.Severity 31 import com.android.tools.metalava.reporter.Severity.ERROR 32 import com.android.tools.metalava.reporter.Severity.HIDDEN 33 import com.android.tools.metalava.reporter.Severity.INFO 34 import com.android.tools.metalava.reporter.Severity.INHERIT 35 import com.android.tools.metalava.reporter.Severity.WARNING 36 import com.android.tools.metalava.reporter.Severity.WARNING_ERROR_WHEN_NEW 37 import java.io.File 38 import java.io.OutputStreamWriter 39 import java.io.PrintWriter 40 import java.nio.file.Path 41 42 internal class DefaultReporter( 43 private val environment: ReporterEnvironment, 44 private val issueConfiguration: IssueConfiguration, 45 46 /** [Baseline] file associated with this [Reporter]. */ 47 private val baseline: Baseline? = null, 48 49 /** 50 * An error message associated with this [Reporter], which should be shown to the user when 51 * metalava finishes with errors. 52 */ 53 private val errorMessage: String? = null, 54 55 /** Filter to hide issues reported in packages which are not part of the API. */ 56 private val packageFilter: PackageFilter? = null, 57 58 /** Additional config properties. */ 59 private val config: Config = Config(), 60 ) : Reporter { 61 private var errors = mutableListOf<String>() 62 private var warningCount = 0 63 64 /** 65 * Configuration properties for the reporter. 66 * 67 * This contains properties that are shared across all instances of [DefaultReporter], except 68 * for the bootstrapping reporter. That receives a default instance of this. 69 */ 70 class Config( 71 /** If true, treat all warnings as errors */ 72 val warningsAsErrors: Boolean = false, 73 74 /** Whether output should be colorized */ 75 val terminal: Terminal = plainTerminal, 76 77 /** 78 * Optional writer to which, if present, all errors, even if they were suppressed in 79 * baseline or via annotation, will be written. 80 */ 81 val reportEvenIfSuppressedWriter: PrintWriter? = null, 82 ) 83 84 /** The number of errors. */ 85 val errorCount 86 get() = errors.size 87 88 /** Returns whether any errors have been detected. */ hasErrorsnull89 fun hasErrors(): Boolean = errors.size > 0 90 91 override fun report( 92 id: Issues.Issue, 93 reportable: Reportable?, 94 message: String, 95 location: FileLocation, 96 maximumSeverity: Severity, 97 ): Boolean { 98 val severity = issueConfiguration.getSeverity(id) 99 val upgradedSeverity = 100 if (severity == WARNING && config.warningsAsErrors) { 101 ERROR 102 } else { 103 severity 104 } 105 106 // Limit the Severity to the maximum allowed. 107 val effectiveSeverity = minOf(upgradedSeverity, maximumSeverity) 108 if (effectiveSeverity == HIDDEN) { 109 return false 110 } 111 112 fun dispatch( 113 which: 114 ( 115 severity: Severity, location: String?, message: String, id: Issues.Issue 116 ) -> Boolean 117 ): Boolean { 118 // When selecting a location to use for reporting the issue the location is used in 119 // preference to the item because the location is more specific. e.g. if the item is a 120 // method then the location may be a line within the body of the method. 121 val reportLocation = 122 when { 123 location.path != null -> location 124 else -> reportable?.fileLocation 125 } 126 127 return which(effectiveSeverity, reportLocation?.forReport(), message, id) 128 } 129 130 // Optionally write to the --report-even-if-suppressed file. 131 dispatch(this::reportEvenIfSuppressed) 132 133 if (isSuppressed(id, reportable, message)) { 134 return false 135 } 136 137 // If we are only emitting some packages (--stub-packages), don't report 138 // issues from other packages 139 val item = reportable as? Item 140 if (item != null) { 141 if (packageFilter != null) { 142 val pkg = (item as? PackageItem) ?: item.containingPackage() 143 if (pkg != null && !packageFilter.matches(pkg)) { 144 return false 145 } 146 } 147 } 148 149 if (baseline != null) { 150 // When selecting a location to use for in checking the baseline the item is used in 151 // preference to the location because the item is more stable. e.g. the location may be 152 // for a specific line within a method which would change over time while the method 153 // signature would stay the same. 154 val baselineKey = 155 when { 156 // When available use the baseline key from the reportable. 157 reportable != null -> reportable.baselineKey 158 // Otherwise, use the baseline key from the file location. 159 else -> location.baselineKey 160 } 161 162 if (baselineKey != null && baseline.mark(baselineKey, message, id)) return false 163 } 164 165 return dispatch(this::doReport) 166 } 167 isSuppressednull168 override fun isSuppressed( 169 id: Issues.Issue, 170 reportable: Reportable?, 171 message: String? 172 ): Boolean { 173 val severity = issueConfiguration.getSeverity(id) 174 if (severity == HIDDEN) { 175 return true 176 } 177 178 reportable ?: return false 179 180 // Suppress the issue if requested for the item. 181 return reportable.suppressedIssues().any { suppressMatches(it, id.name, message) } 182 } 183 suppressMatchesnull184 private fun suppressMatches(value: String, id: String?, message: String?): Boolean { 185 id ?: return false 186 187 if (value == id) { 188 return true 189 } 190 191 if ( 192 message != null && 193 value.startsWith(id) && 194 value.endsWith(message) && 195 (value == "$id:$message" || value == "$id: $message") 196 ) { 197 return true 198 } 199 200 return false 201 } 202 203 /** 204 * Relativize the [absolutePath] against the [ReporterEnvironment.rootFolder] if specified. 205 * 206 * Tests will set [rootFolder] to the temporary directory so that this can remove that from any 207 * paths that are reported to avoid the test having to be aware of the temporary directory. 208 */ relativizeLocationPathnull209 private fun relativizeLocationPath(absolutePath: Path): String { 210 // b/255575766: Note that [relativize] requires two paths to compare to have same types: 211 // either both of them are absolute paths or both of them are not absolute paths. 212 val path = environment.rootFolder.toPath().relativize(absolutePath) ?: absolutePath 213 return path.toString() 214 } 215 216 /** 217 * Convert the [FileLocation] to an optional string representation suitable for use in a report. 218 * 219 * See [relativizeLocationPath]. 220 */ forReportnull221 private fun FileLocation.forReport(): String? { 222 val pathString = path?.let { relativizeLocationPath(it) } ?: return null 223 return if (line > 0) "$pathString:$line" else pathString 224 } 225 226 /** Alias to allow method reference to `dispatch` in [report] */ doReportnull227 private fun doReport( 228 severity: Severity, 229 location: String?, 230 message: String, 231 id: Issues.Issue?, 232 ): Boolean { 233 val terminal: Terminal = config.terminal 234 val formattedMessage = format(severity, location, message, id, terminal) 235 if (severity == ERROR) { 236 errors.add(formattedMessage) 237 } else if (severity == WARNING) { 238 warningCount++ 239 } 240 241 environment.printReport(formattedMessage, severity) 242 return true 243 } 244 formatnull245 private fun format( 246 severity: Severity, 247 location: String?, 248 message: String, 249 id: Issues.Issue?, 250 terminal: Terminal, 251 ): String { 252 val sb = StringBuilder(100) 253 254 sb.append(terminal.attributes(bold = true)) 255 location?.let { sb.append(it).append(": ") } 256 when (severity) { 257 INFO -> sb.append(terminal.attributes(foreground = TerminalColor.CYAN)).append("info: ") 258 WARNING, 259 WARNING_ERROR_WHEN_NEW -> 260 sb.append(terminal.attributes(foreground = TerminalColor.YELLOW)) 261 .append("warning: ") 262 ERROR -> 263 sb.append(terminal.attributes(foreground = TerminalColor.RED)).append("error: ") 264 INHERIT, 265 HIDDEN -> {} 266 } 267 sb.append(terminal.reset()) 268 sb.append(message) 269 sb.append(severity.messageSuffix) 270 id?.let { sb.append(" [").append(it.name).append("]") } 271 return sb.toString() 272 } 273 reportEvenIfSuppressednull274 private fun reportEvenIfSuppressed( 275 severity: Severity, 276 location: String?, 277 message: String, 278 id: Issues.Issue 279 ): Boolean { 280 config.reportEvenIfSuppressedWriter?.println( 281 format(severity, location, message, id, terminal = plainTerminal) 282 ) 283 return true 284 } 285 286 /** Print all the recorded errors to the given writer. Returns the number of errors printer. */ printErrorsnull287 fun printErrors(writer: PrintWriter, maxErrors: Int): Int { 288 var i = 0 289 errors.forEach loop@{ 290 if (i >= maxErrors) { 291 return@loop 292 } 293 i++ 294 writer.println(it) 295 } 296 return i 297 } 298 299 /** Write the error message set to this [Reporter], if any errors have been detected. */ writeErrorMessagenull300 fun writeErrorMessage(writer: PrintWriter) { 301 if (hasErrors()) { 302 errorMessage?.let { writer.write(it) } 303 } 304 } 305 getBaselineDescriptionnull306 fun getBaselineDescription(): String { 307 val file = baseline?.file 308 return if (file != null) { 309 "baseline ${file.path}" 310 } else { 311 "no baseline" 312 } 313 } 314 } 315 316 /** 317 * Provides access to information about the environment within which the [Reporter] will be being 318 * used. 319 */ 320 interface ReporterEnvironment { 321 322 /** Root folder, against which location paths will be relativized to simplify the output. */ 323 val rootFolder: File 324 325 /** Print the report. */ printReportnull326 fun printReport(message: String, severity: Severity) 327 } 328 329 class DefaultReporterEnvironment( 330 val stdout: PrintWriter = PrintWriter(OutputStreamWriter(System.out)), 331 val stderr: PrintWriter = PrintWriter(OutputStreamWriter(System.err)), 332 ) : ReporterEnvironment { 333 334 override val rootFolder = File("").absoluteFile 335 336 override fun printReport(message: String, severity: Severity) { 337 val output = if (severity == ERROR) stderr else stdout 338 output.println(message.trim()) 339 output.flush() 340 } 341 } 342