• 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
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