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