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.github.ajalt.clikt.core.CliktCommand
39 import com.github.ajalt.clikt.core.context
40 import com.github.ajalt.clikt.parameters.arguments.argument
41 import com.github.ajalt.clikt.parameters.arguments.multiple
42 import com.github.ajalt.clikt.parameters.groups.provideDelegate
43 import java.io.File
44 import java.io.PrintWriter
45 import java.util.Locale
46 
47 /**
48  * A command that is passed to [MetalavaCommand.defaultCommand] when the main metalava functionality
49  * needs to be run when no subcommand is provided.
50  */
51 class MainCommand(
52     commonOptions: CommonOptions,
53     executionEnvironment: ExecutionEnvironment,
54 ) :
55     CliktCommand(
56         help = "The default sub-command that is run if no sub-command is specified.",
57         treatUnknownOptionsAsArgs = true,
58     ) {
59 
60     init {
61         // Although, the `helpFormatter` is inherited from the parent context unless overridden the
62         // same is not true for the `localization` so make sure to initialize it for this command.
63         context {
64             localization = MetalavaLocalization()
65 
66             // Explicitly specify help options as the parent command disables it.
67             helpOptionNames = setOf("-h", "--help")
68 
69             // Override the help formatter to add in documentation for the legacy flags.
70             helpFormatter =
71                 LegacyHelpFormatter(
72                     { terminal },
73                     localization,
74                     OptionsHelp::getUsage,
75                 )
76         }
77     }
78 
79     /** Property into which all the arguments (and unknown options) are gathered. */
80     private val flags by
81         argument(
82                 name = "flags",
83                 help = "See below.",
84             )
85             .multiple()
86 
87     private val sourceOptions by SourceOptions()
88 
89     /** Issue reporter configuration. */
90     private val issueReportingOptions by
91         IssueReportingOptions(executionEnvironment.reporterEnvironment, 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 apiSelectionOptions by ApiSelectionOptions()
108 
109     /** API lint options. */
110     private val apiLintOptions by
111         ApiLintOptions(
112             executionEnvironment = executionEnvironment,
113             commonBaselineOptions = commonBaselineOptions,
114         )
115 
116     /** Compatibility check options. */
117     private val compatibilityCheckOptions by
118         CompatibilityCheckOptions(
119             executionEnvironment = executionEnvironment,
120             commonBaselineOptions = commonBaselineOptions,
121         )
122 
123     /** Signature file options. */
124     private val signatureFileOptions by SignatureFileOptions()
125 
126     /** Signature format options. */
127     private val signatureFormatOptions by SignatureFormatOptions()
128 
129     /** Stub generation options. */
130     private val stubGenerationOptions by StubGenerationOptions()
131 
132     /**
133      * Add [Options] (an [OptionGroup]) so that any Clikt defined properties will be processed by
134      * Clikt.
135      */
136     internal val optionGroup by
137         Options(
138             commonOptions = commonOptions,
139             sourceOptions = sourceOptions,
140             issueReportingOptions = issueReportingOptions,
141             generalReportingOptions = generalReportingOptions,
142             apiSelectionOptions = apiSelectionOptions,
143             apiLintOptions = apiLintOptions,
144             compatibilityCheckOptions = compatibilityCheckOptions,
145             signatureFileOptions = signatureFileOptions,
146             signatureFormatOptions = signatureFormatOptions,
147             stubGenerationOptions = stubGenerationOptions,
148         )
149 
150     override fun run() {
151         // Make sure to flush out the baseline files, close files and write any final messages.
152         registerPostCommandAction {
153             // Update and close all baseline files.
154             optionGroup.allBaselines.forEach { baseline ->
155                 if (optionGroup.verbose) {
156                     baseline.dumpStats(optionGroup.stdout)
157                 }
158                 if (baseline.close()) {
159                     if (!optionGroup.quiet) {
160                         stdout.println(
161                             "$PROGRAM_NAME wrote updated baseline to ${baseline.updateFile}"
162                         )
163                     }
164                 }
165             }
166 
167             issueReportingOptions.reporterConfig.reportEvenIfSuppressedWriter?.close()
168 
169             // Show failure messages, if any.
170             optionGroup.allReporters.forEach { it.writeErrorMessage(stderr) }
171         }
172 
173         // Get any remaining arguments/options that were not handled by Clikt.
174         val remainingArgs = flags.toTypedArray()
175 
176         // Parse any remaining arguments
177         optionGroup.parse(executionEnvironment, remainingArgs)
178 
179         // Update the global options.
180         @Suppress("DEPRECATION")
181         options = optionGroup
182 
183         val sourceModelProvider =
184             // Use the [SourceModelProvider] specified by the [TestEnvironment], if any.
185             executionEnvironment.testEnvironment?.sourceModelProvider
186             // Otherwise, use the one specified on the command line, or the default.
187             ?: SourceModelProvider.getImplementation(optionGroup.sourceModelProvider)
188         sourceModelProvider
189             .createEnvironmentManager(executionEnvironment.disableStderrDumping())
190             .use { processFlags(executionEnvironment, it, progressTracker) }
191 
192         if (
193             optionGroup.allReporters.any { it.hasErrors() } &&
194                 !commonBaselineOptions.passBaselineUpdates
195         ) {
196             // Repeat the errors at the end to make it easy to find the actual problems.
197             if (issueReportingOptions.repeatErrorsMax > 0) {
198                 repeatErrors(
199                     stderr,
200                     optionGroup.allReporters,
201                     issueReportingOptions.repeatErrorsMax
202                 )
203             }
204 
205             // Make sure that the process exits with an error code.
206             throw MetalavaCliException(exitCode = -1)
207         }
208     }
209 
210     /**
211      * Produce a default file name for the baseline. It's normally "baseline.txt", but can be
212      * prefixed by show annotations; e.g. @TestApi -> test-baseline.txt, @SystemApi ->
213      * system-baseline.txt, etc.
214      *
215      * Note because the default baseline file is not explicitly set in the command line, this file
216      * would trigger a --strict-input-files violation. To avoid that, always explicitly pass a
217      * baseline file.
218      */
219     private fun getDefaultBaselineFile(): File? {
220         val sourcePath = sourceOptions.sourcePath
221         if (sourcePath.isNotEmpty() && sourcePath[0].path.isNotBlank()) {
222             fun annotationToPrefix(qualifiedName: String): String {
223                 val name = qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1)
224                 return name.lowercase(Locale.US).removeSuffix("api") + "-"
225             }
226             val sb = StringBuilder()
227             apiSelectionOptions.allShowAnnotations.getIncludedAnnotationNames().forEach {
228                 sb.append(annotationToPrefix(it))
229             }
230             sb.append(DEFAULT_BASELINE_NAME)
231             var base = sourcePath[0]
232             // Convention: in AOSP, signature files are often in sourcepath/api: let's place
233             // baseline files there too
234             val api = File(base, "api")
235             if (api.isDirectory) {
236                 base = api
237             }
238             return File(base, sb.toString())
239         } else {
240             return null
241         }
242     }
243 }
244 
repeatErrorsnull245 private fun repeatErrors(writer: PrintWriter, reporters: List<DefaultReporter>, max: Int) {
246     writer.println("Error: $PROGRAM_NAME detected the following problems:")
247     val totalErrors = reporters.sumOf { it.errorCount }
248     var remainingCap = max
249     var totalShown = 0
250     reporters.forEach {
251         val numShown = it.printErrors(writer, remainingCap)
252         remainingCap -= numShown
253         totalShown += numShown
254     }
255     if (totalShown < totalErrors) {
256         writer.println(
257             "${totalErrors - totalShown} more error(s) omitted. Search the log for 'error:' to find all of them."
258         )
259     }
260 }
261