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
18
19 import com.android.tools.metalava.cli.common.CommonBaselineOptions
20 import com.android.tools.metalava.cli.common.CommonOptions
21 import com.android.tools.metalava.cli.common.ExecutionEnvironment
22 import com.android.tools.metalava.cli.common.IssueReportingOptions
23 import com.android.tools.metalava.cli.common.LegacyHelpFormatter
24 import com.android.tools.metalava.cli.common.MetalavaCliException
25 import com.android.tools.metalava.cli.common.MetalavaLocalization
26 import com.android.tools.metalava.cli.common.SourceOptions
27 import com.android.tools.metalava.cli.common.executionEnvironment
28 import com.android.tools.metalava.cli.common.progressTracker
29 import com.android.tools.metalava.cli.common.registerPostCommandAction
30 import com.android.tools.metalava.cli.common.stderr
31 import com.android.tools.metalava.cli.common.stdout
32 import com.android.tools.metalava.cli.common.terminal
33 import com.android.tools.metalava.cli.compatibility.CompatibilityCheckOptions
34 import com.android.tools.metalava.cli.lint.ApiLintOptions
35 import com.android.tools.metalava.cli.signature.SignatureFormatOptions
36 import com.android.tools.metalava.model.source.SourceModelProvider
37 import com.android.tools.metalava.reporter.DEFAULT_BASELINE_NAME
38 import com.android.tools.metalava.reporter.DefaultReporter
39 import com.github.ajalt.clikt.core.CliktCommand
40 import com.github.ajalt.clikt.core.context
41 import com.github.ajalt.clikt.parameters.arguments.argument
42 import com.github.ajalt.clikt.parameters.arguments.multiple
43 import com.github.ajalt.clikt.parameters.groups.provideDelegate
44 import java.io.File
45 import java.io.PrintWriter
46 import java.util.Locale
47
48 /**
49 * A command that is passed to [MetalavaCommand.defaultCommand] when the main metalava functionality
50 * needs to be run when no subcommand is provided.
51 */
52 class MainCommand(
53 commonOptions: CommonOptions,
54 executionEnvironment: ExecutionEnvironment,
55 ) :
56 CliktCommand(
57 help = "The default sub-command that is run if no sub-command is specified.",
58 treatUnknownOptionsAsArgs = true,
59 ) {
60
61 init {
62 // Although, the `helpFormatter` is inherited from the parent context unless overridden the
63 // same is not true for the `localization` so make sure to initialize it for this command.
64 context {
65 localization = MetalavaLocalization()
66
67 // Explicitly specify help options as the parent command disables it.
68 helpOptionNames = setOf("-h", "--help")
69
70 // Override the help formatter to add in documentation for the legacy flags.
71 helpFormatter =
72 LegacyHelpFormatter(
73 { terminal },
74 localization,
75 OptionsHelp::getUsage,
76 )
77 }
78 }
79
80 /** Property into which all the arguments (and unknown options) are gathered. */
81 private val flags by
82 argument(
83 name = "flags",
84 help = "See below.",
85 )
86 .multiple()
87
88 private val sourceOptions by SourceOptions()
89
90 /** Issue reporter configuration. */
91 private val issueReportingOptions by IssueReportingOptions(commonOptions)
92
93 private val commonBaselineOptions by
94 CommonBaselineOptions(
95 sourceOptions = sourceOptions,
96 issueReportingOptions = issueReportingOptions,
97 )
98
99 /** General reporter options. */
100 private val generalReportingOptions by
101 GeneralReportingOptions(
102 executionEnvironment = executionEnvironment,
103 commonBaselineOptions = commonBaselineOptions,
104 defaultBaselineFileProvider = { getDefaultBaselineFile() },
105 )
106
107 private val configFileOptions by ConfigFileOptions()
108
109 private val apiSelectionOptions: ApiSelectionOptions by
110 ApiSelectionOptions(
111 apiSurfacesConfigProvider = { configFileOptions.config.apiSurfaces },
112 checkSurfaceConsistencyProvider = {
113 val sources = optionGroup.sources
114 // The --show-unannotated and --show*-annotation options affect the ApiSurfaces that
115 // is used. As do the --api-surface and API surfaces defined in a config file. In
116 // the long term the former will be discarded in favor of the latter but during the
117 // transition it is important that they are consistent. Consistency is important
118 // when the --show* options are significant, i.e. affect the output of Metalava.
119 // Unfortunately, they can be significant even if they are not specified, i.e. if
120 // none of them are specified then it behaves as if --show-unannotated was specified
121 // and depending on other options they may be significant or not.
122 //
123 // The --show* options are always significant if sources are provided, and they are
124 // not signature files or jar files. If they are signature files then the --show*
125 // options are not significant because signature files are already pre-filtered. If
126 // they are jar files then they are almost certainly stubs and so the --show*
127 // options are not significant because stub jar files are are also already
128 // pre-filtered.
129 sources.isNotEmpty() &&
130 sources[0].extension.let { extension ->
131 extension != "jar" && extension != "txt"
132 }
133 },
134 )
135
136 /** API lint options. */
137 private val apiLintOptions by
138 ApiLintOptions(
139 executionEnvironment = executionEnvironment,
140 commonBaselineOptions = commonBaselineOptions,
141 )
142
143 /** Compatibility check options. */
144 private val compatibilityCheckOptions by
145 CompatibilityCheckOptions(
146 executionEnvironment = executionEnvironment,
147 commonBaselineOptions = commonBaselineOptions,
148 )
149
150 /** Signature file options. */
151 private val signatureFileOptions by SignatureFileOptions()
152
153 /** Signature format options. */
154 private val signatureFormatOptions by SignatureFormatOptions()
155
156 /** Stub generation options. */
157 private val stubGenerationOptions by StubGenerationOptions()
158
159 /** Api levels generation options. */
160 private val apiLevelsGenerationOptions by
161 ApiLevelsGenerationOptions(
162 executionEnvironment = executionEnvironment,
163 earlyOptions = commonOptions,
164 apiSurfacesProvider = { apiSelectionOptions.apiSurfaces },
165 )
166
167 /**
168 * Add [Options] (an [OptionGroup]) so that any Clikt defined properties will be processed by
169 * Clikt.
170 */
171 internal val optionGroup by
172 Options(
173 executionEnvironment = executionEnvironment,
174 commonOptions = commonOptions,
175 sourceOptions = sourceOptions,
176 issueReportingOptions = issueReportingOptions,
177 generalReportingOptions = generalReportingOptions,
178 configFileOptions = configFileOptions,
179 apiSelectionOptions = apiSelectionOptions,
180 apiLintOptions = apiLintOptions,
181 compatibilityCheckOptions = compatibilityCheckOptions,
182 signatureFileOptions = signatureFileOptions,
183 signatureFormatOptions = signatureFormatOptions,
184 stubGenerationOptions = stubGenerationOptions,
185 apiLevelsGenerationOptions = apiLevelsGenerationOptions,
186 )
187
188 override fun run() {
189 // Make sure to flush out the baseline files, close files and write any final messages.
190 registerPostCommandAction {
191 // Update and close all baseline files.
192 optionGroup.allBaselines.forEach { baseline ->
193 if (optionGroup.verbose) {
194 baseline.dumpStats(optionGroup.stdout)
195 }
196 if (baseline.close()) {
197 if (!optionGroup.quiet) {
198 stdout.println(
199 "$PROGRAM_NAME wrote updated baseline to ${baseline.updateFile}"
200 )
201 }
202 }
203 }
204
205 issueReportingOptions.reporterConfig.reportEvenIfSuppressedWriter?.close()
206
207 // Show failure messages, if any.
208 optionGroup.allReporters.forEach { it.writeErrorMessage(stderr) }
209 }
210
211 // Get any remaining arguments/options that were not handled by Clikt.
212 val remainingArgs = flags.toTypedArray()
213
214 // Parse any remaining arguments
215 optionGroup.parse(remainingArgs)
216
217 // Update the global options.
218 @Suppress("DEPRECATION")
219 options = optionGroup
220
221 val sourceModelProvider =
222 // Use the [SourceModelProvider] specified by the [TestEnvironment], if any.
223 executionEnvironment.testEnvironment?.sourceModelProvider
224 // Otherwise, use the one specified on the command line, or the default.
225 ?: SourceModelProvider.getImplementation(optionGroup.sourceModelProvider)
226
227 try {
228 sourceModelProvider
229 .createEnvironmentManager(executionEnvironment.disableStderrDumping())
230 .use { processFlags(executionEnvironment, it, progressTracker) }
231 } finally {
232 // Write all saved reports. Do this even if the previous code threw an exception.
233 optionGroup.allReporters.forEach { it.writeSavedReports() }
234 }
235
236 val allReporters = optionGroup.allReporters
237 if (allReporters.any { it.hasErrors() } && !commonBaselineOptions.passBaselineUpdates) {
238 // Repeat the errors at the end to make it easy to find the actual problems.
239 if (issueReportingOptions.repeatErrorsMax > 0) {
240 repeatErrors(stderr, allReporters, issueReportingOptions.repeatErrorsMax)
241 }
242
243 // Make sure that the process exits with an error code.
244 throw MetalavaCliException(exitCode = -1)
245 }
246 }
247
248 /**
249 * Produce a default file name for the baseline. It's normally "baseline.txt", but can be
250 * prefixed by show annotations; e.g. @TestApi -> test-baseline.txt, @SystemApi ->
251 * system-baseline.txt, etc.
252 *
253 * Note because the default baseline file is not explicitly set in the command line, this file
254 * would trigger a --strict-input-files violation. To avoid that, always explicitly pass a
255 * baseline file.
256 */
257 private fun getDefaultBaselineFile(): File? {
258 val sourcePath = sourceOptions.sourcePath
259 if (sourcePath.isNotEmpty() && sourcePath[0].path.isNotBlank()) {
260 fun annotationToPrefix(qualifiedName: String): String {
261 val name = qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1)
262 return name.lowercase(Locale.US).removeSuffix("api") + "-"
263 }
264 val sb = StringBuilder()
265 apiSelectionOptions.allShowAnnotations.getIncludedAnnotationNames().forEach {
266 sb.append(annotationToPrefix(it))
267 }
268 sb.append(DEFAULT_BASELINE_NAME)
269 var base = sourcePath[0]
270 // Convention: in AOSP, signature files are often in sourcepath/api: let's place
271 // baseline files there too
272 val api = File(base, "api")
273 if (api.isDirectory) {
274 base = api
275 }
276 return File(base, sb.toString())
277 } else {
278 return null
279 }
280 }
281 }
282
repeatErrorsnull283 private fun repeatErrors(writer: PrintWriter, reporters: List<DefaultReporter>, max: Int) {
284 writer.println("Error: $PROGRAM_NAME detected the following problems:")
285 val totalErrors = reporters.sumOf { it.errorCount }
286 var remainingCap = max
287 var totalShown = 0
288 reporters.forEach {
289 val numShown = it.printErrors(writer, remainingCap)
290 remainingCap -= numShown
291 totalShown += numShown
292 }
293 if (totalShown < totalErrors) {
294 writer.println(
295 "${totalErrors - totalShown} more error(s) omitted. Search the log for 'error:' to find all of them."
296 )
297 }
298 }
299