• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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