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