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.cli.common 18 19 import com.android.tools.metalava.DefaultReporter 20 import com.android.tools.metalava.DefaultReporterEnvironment 21 import com.android.tools.metalava.ReporterEnvironment 22 import com.android.tools.metalava.reporter.ERROR_WHEN_NEW_SUFFIX 23 import com.android.tools.metalava.reporter.IssueConfiguration 24 import com.android.tools.metalava.reporter.Issues 25 import com.android.tools.metalava.reporter.Reporter 26 import com.android.tools.metalava.reporter.Severity 27 import com.github.ajalt.clikt.parameters.groups.OptionGroup 28 import com.github.ajalt.clikt.parameters.options.default 29 import com.github.ajalt.clikt.parameters.options.flag 30 import com.github.ajalt.clikt.parameters.options.option 31 import com.github.ajalt.clikt.parameters.types.int 32 import com.github.ajalt.clikt.parameters.types.restrictTo 33 import java.io.File 34 35 const val ARG_ERROR = "--error" 36 const val ARG_ERROR_WHEN_NEW = "--error-when-new" 37 const val ARG_WARNING = "--warning" 38 const val ARG_HIDE = "--hide" 39 const val ARG_ERROR_CATEGORY = "--error-category" 40 const val ARG_ERROR_WHEN_NEW_CATEGORY = "--error-when-new-category" 41 const val ARG_WARNING_CATEGORY = "--warning-category" 42 const val ARG_HIDE_CATEGORY = "--hide-category" 43 44 const val ARG_WARNINGS_AS_ERRORS = "--warnings-as-errors" 45 46 const val ARG_REPORT_EVEN_IF_SUPPRESSED = "--report-even-if-suppressed" 47 48 /** The name of the group, can be used in help text to refer to the options in this group. */ 49 const val REPORTING_OPTIONS_GROUP = "Issue Reporting" 50 51 class IssueReportingOptions( 52 reporterEnvironment: ReporterEnvironment = DefaultReporterEnvironment(), 53 commonOptions: CommonOptions = CommonOptions(), 54 ) : 55 OptionGroup( 56 name = REPORTING_OPTIONS_GROUP, 57 help = 58 """ 59 Options that control which issues are reported, the severity of the reports, how, when 60 and where they are reported. 61 62 See `metalava help issues` for more help including a table of the available issues and 63 their category and default severity. 64 """ 65 .trimIndent() 66 ) { 67 68 /** The [IssueConfiguration] that is configured by these options. */ 69 val issueConfiguration = IssueConfiguration() 70 71 /** 72 * The [Reporter] that is used to report issues encountered while parsing these options. 73 * 74 * A slight complexity is that this [Reporter] and its [IssueConfiguration] are both modified 75 * and used during the process of processing the options. 76 */ 77 internal val bootstrapReporter: Reporter = 78 DefaultReporter( 79 reporterEnvironment, 80 issueConfiguration, 81 ) 82 83 init { 84 // Create a Clikt option for handling the issue options and updating them as a side effect. 85 // This needs to be a single option for handling all the issue options in one go because the 86 // order of processing them matters and Clikt will collate all the values for all the 87 // options together before processing them so something like this would never work if they 88 // were treated as separate options. 89 // --hide Foo --error Bar --hide Bar --error Foo 90 // 91 // When processed immediately that is equivalent to: 92 // --hide Bar --error Foo 93 // 94 // However, when processed after grouping they would be equivalent to one of the following 95 // depending on which was processed last: 96 // --hide Foo,Bar 97 // --error Foo,Bar 98 // 99 // Instead, this creates a single Clikt option to handle all the issue options but they are 100 // still collated before processing so if they are interleaved with other options whose 101 // parsing makes use of the `reporter` then there is the potential for a change in behavior. 102 // However, currently it does not look as though that is the case except for reporting 103 // issues with deprecated options which is tested. 104 // 105 // Having a single Clikt option with lots of different option names does make the help hard 106 // to read as it produces a single line with all the option names on it. So, this uses a 107 // mechanism that will cause `MetalavaHelpFormatter` to split the option into multiple 108 // separate options purely for help purposes. 109 val issueOption = 110 compositeSideEffectOption( 111 // Create one side effect option per label. <lambda>null112 ConfigLabel.values().map { 113 sideEffectOption(it.optionName, help = it.help) { 114 // if `--hide id1,id2` was supplied on the command line then this will split 115 // it into 116 // ["id1", "id2"] 117 val values = it.split(",") 118 119 // Get the label from the name of the option. 120 val label = ConfigLabel.fromOptionName(name) 121 122 // Update the configuration immediately 123 values.forEach { 124 label.setAspectForId(bootstrapReporter, issueConfiguration, it.trim()) 125 } 126 } 127 } 128 ) 129 130 // Register the option so that Clikt will process it. 131 registerOption(issueOption) 132 } 133 134 private val warningsAsErrors: Boolean by 135 option( 136 ARG_WARNINGS_AS_ERRORS, 137 help = 138 """ 139 Promote all warnings to errors. 140 """ 141 .trimIndent() 142 ) 143 .flag() 144 145 /** Writes a list of all errors, even if they were suppressed in baseline or via annotation. */ 146 private val reportEvenIfSuppressedFile by 147 option( 148 ARG_REPORT_EVEN_IF_SUPPRESSED, 149 help = 150 """ 151 Write all issues into the given file, even if suppressed (via annotation or 152 baseline) but not if hidden (by '$ARG_HIDE' or '$ARG_HIDE_CATEGORY'). 153 """ 154 .trimIndent(), 155 ) 156 .newOrExistingFile() 157 158 /** When non-0, metalava repeats all the errors at the end of the run, at most this many. */ 159 val repeatErrorsMax by 160 option( 161 ARG_REPEAT_ERRORS_MAX, 162 metavar = "<n>", 163 help = """When specified, repeat at most N errors before finishing.""" 164 ) 165 .int() 166 .restrictTo(min = 0) 167 .default(0) 168 169 internal val reporterConfig by <lambda>null170 lazy(LazyThreadSafetyMode.NONE) { 171 val reportEvenIfSuppressedWriter = reportEvenIfSuppressedFile?.printWriter() 172 173 DefaultReporter.Config( 174 warningsAsErrors = warningsAsErrors, 175 terminal = commonOptions.terminal, 176 reportEvenIfSuppressedWriter = reportEvenIfSuppressedWriter, 177 ) 178 } 179 } 180 181 /** The different configurable aspects of [IssueConfiguration]. */ 182 private enum class ConfigurableAspect { 183 /** A single issue needs configuring. */ 184 ISSUE { setAspectSeverityForIdnull185 override fun setAspectSeverityForId( 186 reporter: Reporter, 187 configuration: IssueConfiguration, 188 optionName: String, 189 severity: Severity, 190 id: String 191 ) { 192 val issue = 193 Issues.findIssueById(id) 194 ?: Issues.findIssueByIdIgnoringCase(id)?.also { 195 reporter.report( 196 Issues.DEPRECATED_OPTION, 197 null as File?, 198 "Case-insensitive issue matching is deprecated, use " + 199 "$optionName ${it.name} instead of $optionName $id" 200 ) 201 } 202 ?: throw MetalavaCliException("Unknown issue id: '$optionName' '$id'") 203 204 configuration.setSeverity(issue, severity) 205 } 206 }, 207 /** A whole category of issues needs configuring. */ 208 CATEGORY { setAspectSeverityForIdnull209 override fun setAspectSeverityForId( 210 reporter: Reporter, 211 configuration: IssueConfiguration, 212 optionName: String, 213 severity: Severity, 214 id: String 215 ) { 216 val issues = 217 Issues.findCategoryById(id)?.let { Issues.findIssuesByCategory(it) } 218 ?: throw MetalavaCliException("Unknown category: $optionName $id") 219 220 issues.forEach { configuration.setSeverity(it, severity) } 221 } 222 }; 223 224 /** Configure the [IssueConfiguration] appropriately. */ setAspectSeverityForIdnull225 abstract fun setAspectSeverityForId( 226 reporter: Reporter, 227 configuration: IssueConfiguration, 228 optionName: String, 229 severity: Severity, 230 id: String 231 ) 232 } 233 234 /** The different labels that can be used on the command line. */ 235 private enum class ConfigLabel( 236 val optionName: String, 237 /** The [Severity] which this label corresponds to. */ 238 val severity: Severity, 239 val aspect: ConfigurableAspect, 240 val help: String 241 ) { 242 ERROR( 243 ARG_ERROR, 244 Severity.ERROR, 245 ConfigurableAspect.ISSUE, 246 "Report issues of the given id as errors.", 247 ), 248 ERROR_WHEN_NEW( 249 ARG_ERROR_WHEN_NEW, 250 Severity.WARNING_ERROR_WHEN_NEW, 251 ConfigurableAspect.ISSUE, 252 """ 253 Report issues of the given id as warnings in existing code and errors in new code. The 254 latter behavior relies on infrastructure that handles checking changes to the code 255 detecting the ${ERROR_WHEN_NEW_SUFFIX.trim()} text in the output and preventing the 256 change from being made. 257 """, 258 ), 259 WARNING( 260 ARG_WARNING, 261 Severity.WARNING, 262 ConfigurableAspect.ISSUE, 263 "Report issues of the given id as warnings.", 264 ), 265 HIDE( 266 ARG_HIDE, 267 Severity.HIDDEN, 268 ConfigurableAspect.ISSUE, 269 "Hide/skip issues of the given id.", 270 ), 271 ERROR_CATEGORY( 272 ARG_ERROR_CATEGORY, 273 Severity.ERROR, 274 ConfigurableAspect.CATEGORY, 275 "Report all issues in the given category as errors.", 276 ), 277 ERROR_WHEN_NEW_CATEGORY( 278 ARG_ERROR_WHEN_NEW_CATEGORY, 279 Severity.WARNING_ERROR_WHEN_NEW, 280 ConfigurableAspect.CATEGORY, 281 "Report all issues in the given category as errors-when-new.", 282 ), 283 WARNING_CATEGORY( 284 ARG_WARNING_CATEGORY, 285 Severity.WARNING, 286 ConfigurableAspect.CATEGORY, 287 "Report all issues in the given category as warnings.", 288 ), 289 HIDE_CATEGORY( 290 ARG_HIDE_CATEGORY, 291 Severity.HIDDEN, 292 ConfigurableAspect.CATEGORY, 293 "Hide/skip all issues in the given category.", 294 ); 295 296 /** Configure the aspect identified by [id] into the [configuration]. */ 297 fun setAspectForId(reporter: Reporter, configuration: IssueConfiguration, id: String) { 298 aspect.setAspectSeverityForId(reporter, configuration, optionName, severity, id) 299 } 300 301 companion object { 302 private val optionNameToLabel = ConfigLabel.values().associateBy { it.optionName } 303 304 /** 305 * Get the label for the option name. This is only called with an option name that has been 306 * obtained from [ConfigLabel.optionName] so it is known that it must match. 307 */ 308 fun fromOptionName(option: String): ConfigLabel = optionNameToLabel[option]!! 309 } 310 } 311