1 /*
<lambda>null2  * Copyright (C) 2017 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 @file:JvmName("Driver")
17 
18 package com.android.tools.metalava
19 
20 import com.android.SdkConstants.DOT_JAR
21 import com.android.SdkConstants.DOT_TXT
22 import com.android.tools.metalava.apilevels.ApiGenerator
23 import com.android.tools.metalava.cli.common.ActionContext
24 import com.android.tools.metalava.cli.common.EarlyOptions
25 import com.android.tools.metalava.cli.common.ExecutionEnvironment
26 import com.android.tools.metalava.cli.common.MetalavaCliException
27 import com.android.tools.metalava.cli.common.MetalavaCommand
28 import com.android.tools.metalava.cli.common.SignatureFileLoader
29 import com.android.tools.metalava.cli.common.VersionCommand
30 import com.android.tools.metalava.cli.common.commonOptions
31 import com.android.tools.metalava.cli.compatibility.ARG_CHECK_COMPATIBILITY_BASE_API
32 import com.android.tools.metalava.cli.compatibility.CompatibilityCheckOptions.CheckRequest
33 import com.android.tools.metalava.cli.help.HelpCommand
34 import com.android.tools.metalava.cli.internal.MakeAnnotationsPackagePrivateCommand
35 import com.android.tools.metalava.cli.signature.MergeSignaturesCommand
36 import com.android.tools.metalava.cli.signature.SignatureToDexCommand
37 import com.android.tools.metalava.cli.signature.SignatureToJDiffCommand
38 import com.android.tools.metalava.cli.signature.UpdateSignatureHeaderCommand
39 import com.android.tools.metalava.compatibility.CompatibilityCheck
40 import com.android.tools.metalava.doc.DocAnalyzer
41 import com.android.tools.metalava.lint.ApiLint
42 import com.android.tools.metalava.model.ClassItem
43 import com.android.tools.metalava.model.ClassResolver
44 import com.android.tools.metalava.model.Codebase
45 import com.android.tools.metalava.model.ItemVisitor
46 import com.android.tools.metalava.model.MergedCodebase
47 import com.android.tools.metalava.model.ModelOptions
48 import com.android.tools.metalava.model.psi.PsiModelOptions
49 import com.android.tools.metalava.model.source.EnvironmentManager
50 import com.android.tools.metalava.model.source.SourceParser
51 import com.android.tools.metalava.model.source.SourceSet
52 import com.android.tools.metalava.model.text.ApiClassResolution
53 import com.android.tools.metalava.model.text.SignatureFile
54 import com.android.tools.metalava.reporter.Issues
55 import com.android.tools.metalava.reporter.Reporter
56 import com.android.tools.metalava.stub.StubWriter
57 import com.github.ajalt.clikt.core.subcommands
58 import com.google.common.base.Stopwatch
59 import java.io.File
60 import java.io.IOException
61 import java.io.PrintWriter
62 import java.io.StringWriter
63 import java.util.Arrays
64 import java.util.concurrent.TimeUnit.SECONDS
65 import kotlin.system.exitProcess
66 
67 const val PROGRAM_NAME = "metalava"
68 
69 fun main(args: Array<String>) {
70     val executionEnvironment = ExecutionEnvironment()
71     val exitCode = run(executionEnvironment = executionEnvironment, originalArgs = args)
72 
73     executionEnvironment.stdout.flush()
74     executionEnvironment.stderr.flush()
75 
76     exitProcess(exitCode)
77 }
78 
79 /**
80  * The metadata driver is a command line interface to extracting various metadata from a source tree
81  * (or existing signature files etc.). Run with --help to see more details.
82  */
runnull83 fun run(
84     executionEnvironment: ExecutionEnvironment,
85     originalArgs: Array<String>,
86 ): Int {
87     val stdout = executionEnvironment.stdout
88     val stderr = executionEnvironment.stderr
89 
90     // Preprocess the arguments by adding any additional arguments specified in environment
91     // variables.
92     val modifiedArgs = preprocessArgv(executionEnvironment, originalArgs)
93 
94     // Process the early options. This does not consume any arguments, they will be parsed again
95     // later. A little inefficient but produces cleaner code.
96     val earlyOptions = EarlyOptions.parse(modifiedArgs)
97 
98     val progressTracker = ProgressTracker(earlyOptions.verbosity.verbose, stdout)
99 
100     progressTracker.progress("$PROGRAM_NAME started\n")
101 
102     // Dump the arguments, and maybe generate a rerun-script.
103     maybeDumpArgv(executionEnvironment, originalArgs, modifiedArgs)
104 
105     // Actual work begins here.
106     val command =
107         createMetalavaCommand(
108             executionEnvironment,
109             progressTracker,
110         )
111     val exitCode = command.process(modifiedArgs)
112 
113     stdout.flush()
114     stderr.flush()
115 
116     progressTracker.progress("$PROGRAM_NAME exiting with exit code $exitCode\n")
117 
118     return exitCode
119 }
120 
121 @Suppress("DEPRECATION")
processFlagsnull122 internal fun processFlags(
123     executionEnvironment: ExecutionEnvironment,
124     environmentManager: EnvironmentManager,
125     progressTracker: ProgressTracker
126 ) {
127     val stopwatch = Stopwatch.createStarted()
128 
129     val reporter = options.reporter
130     val reporterApiLint = options.reporterApiLint
131     val annotationManager = options.annotationManager
132     val modelOptions =
133         // If the option was specified on the command line then use [ModelOptions] created from
134         // that.
135         options.useK2Uast?.let { useK2Uast ->
136             ModelOptions.build("from command line") { this[PsiModelOptions.useK2Uast] = useK2Uast }
137         }
138         // Otherwise, use the [ModelOptions] specified in the [TestEnvironment] if any.
139         ?: executionEnvironment.testEnvironment?.modelOptions?.apply {
140                 // Make sure that the [options.useK2Uast] matches the test environment.
141                 options.useK2Uast = this[PsiModelOptions.useK2Uast]
142             }
143             // Otherwise, use the default
144             ?: ModelOptions.empty
145     val sourceParser =
146         environmentManager.createSourceParser(
147             reporter = reporter,
148             annotationManager = annotationManager,
149             javaLanguageLevel = options.javaLanguageLevelAsString,
150             kotlinLanguageLevel = options.kotlinLanguageLevelAsString,
151             modelOptions = modelOptions,
152             allowReadingComments = options.allowReadingComments,
153             jdkHome = options.jdkHome,
154         )
155 
156     val signatureFileCache = options.signatureFileCache
157 
158     val actionContext =
159         ActionContext(
160             progressTracker = progressTracker,
161             reporter = reporter,
162             reporterApiLint = reporterApiLint,
163             sourceParser = sourceParser,
164         )
165 
166     val classResolverProvider =
167         ClassResolverProvider(
168             sourceParser = sourceParser,
169             apiClassResolution = options.apiClassResolution,
170             classpath = options.classpath,
171         )
172 
173     val sources = options.sources
174     val codebase =
175         if (sources.isNotEmpty() && sources[0].path.endsWith(DOT_TXT)) {
176             // Make sure all the source files have .txt extensions.
177             sources
178                 .firstOrNull { !it.path.endsWith(DOT_TXT) }
179                 ?.let {
180                     throw MetalavaCliException(
181                         "Inconsistent input file types: The first file is of $DOT_TXT, but detected different extension in ${it.path}"
182                     )
183                 }
184             val signatureFileLoader = SignatureFileLoader(annotationManager)
185             val textCodebase =
186                 signatureFileLoader.loadFiles(
187                     SignatureFile.fromFiles(sources),
188                     classResolverProvider.classResolver,
189                 )
190 
191             // If this codebase was loaded in order to generate stubs then they will need some
192             // additional items to be added that were purposely removed from the signature files.
193             if (options.stubsDir != null) {
194                 addMissingItemsRequiredForGeneratingStubs(
195                     sourceParser,
196                     textCodebase,
197                     reporterApiLint
198                 )
199             }
200             textCodebase
201         } else if (sources.size == 1 && sources[0].path.endsWith(DOT_JAR)) {
202             actionContext.loadFromJarFile(sources[0])
203         } else if (sources.isNotEmpty() || options.sourcePath.isNotEmpty()) {
204             actionContext.loadFromSources(signatureFileCache, classResolverProvider)
205         } else {
206             return
207         }
208 
209     progressTracker.progress(
210         "$PROGRAM_NAME analyzed API in ${stopwatch.elapsed(SECONDS)} seconds\n"
211     )
212 
213     options.subtractApi?.let {
214         progressTracker.progress("Subtracting API: ")
215         actionContext.subtractApi(signatureFileCache, codebase, it)
216     }
217 
218     val androidApiLevelXml = options.generateApiLevelXml
219     val apiLevelJars = options.apiLevelJars
220     val apiGenerator = ApiGenerator(signatureFileCache)
221     if (androidApiLevelXml != null && apiLevelJars != null) {
222         assert(options.currentApiLevel != -1)
223 
224         progressTracker.progress(
225             "Generating API levels XML descriptor file, ${androidApiLevelXml.name}: "
226         )
227         val sdkJarRoot = options.sdkJarRoot
228         val sdkInfoFile = options.sdkInfoFile
229         val sdkExtArgs: ApiGenerator.SdkExtensionsArguments? =
230             if (sdkJarRoot != null && sdkInfoFile != null) {
231                 ApiGenerator.SdkExtensionsArguments(
232                     sdkJarRoot,
233                     sdkInfoFile,
234                     options.latestReleasedSdkExtension
235                 )
236             } else {
237                 null
238             }
239         apiGenerator.generateXml(
240             apiLevelJars,
241             options.firstApiLevel,
242             options.currentApiLevel,
243             options.isDeveloperPreviewBuild(),
244             androidApiLevelXml,
245             codebase,
246             sdkExtArgs,
247             options.removeMissingClassesInApiLevels
248         )
249     }
250 
251     if (options.docStubsDir != null || options.enhanceDocumentation) {
252         if (!codebase.supportsDocumentation()) {
253             error("Codebase does not support documentation, so it cannot be enhanced.")
254         }
255         progressTracker.progress("Enhancing docs: ")
256         val docAnalyzer = DocAnalyzer(executionEnvironment, codebase, reporterApiLint)
257         docAnalyzer.enhance()
258         val applyApiLevelsXml = options.applyApiLevelsXml
259         if (applyApiLevelsXml != null) {
260             progressTracker.progress("Applying API levels")
261             docAnalyzer.applyApiLevels(applyApiLevelsXml)
262         }
263     }
264 
265     val apiVersionsJson = options.generateApiVersionsJson
266     val apiVersionNames = options.apiVersionNames
267     if (apiVersionsJson != null && apiVersionNames != null) {
268         progressTracker.progress(
269             "Generating API version history JSON file, ${apiVersionsJson.name}: "
270         )
271 
272         val apiType = ApiType.PUBLIC_API
273         val apiEmit = apiType.getEmitFilter(options.apiPredicateConfig)
274         val apiReference = apiType.getReferenceFilter(options.apiPredicateConfig)
275 
276         apiGenerator.generateJson(
277             // The signature files can be null if the current version is the only version
278             options.apiVersionSignatureFiles ?: emptyList(),
279             codebase,
280             apiVersionsJson,
281             apiVersionNames,
282             apiEmit,
283             apiReference
284         )
285     }
286 
287     // Generate the documentation stubs *before* we migrate nullness information.
288     options.docStubsDir?.let {
289         createStubFiles(
290             progressTracker,
291             it,
292             codebase,
293             docStubs = true,
294         )
295     }
296 
297     // Based on the input flags, generates various output files such
298     // as signature files and/or stubs files
299     options.apiFile?.let { apiFile ->
300         val apiType = ApiType.PUBLIC_API
301         val apiEmit = apiType.getEmitFilter(options.apiPredicateConfig)
302         val apiReference = apiType.getReferenceFilter(options.apiPredicateConfig)
303 
304         createReportFile(progressTracker, codebase, apiFile, "API") { printWriter ->
305             SignatureWriter(
306                 printWriter,
307                 apiEmit,
308                 apiReference,
309                 codebase.preFiltered,
310                 fileFormat = options.signatureFileFormat,
311                 showUnannotated = options.showUnannotated,
312                 apiVisitorConfig = options.apiVisitorConfig
313             )
314         }
315     }
316 
317     options.removedApiFile?.let { apiFile ->
318         val apiType = ApiType.REMOVED
319         val removedEmit = apiType.getEmitFilter(options.apiPredicateConfig)
320         val removedReference = apiType.getReferenceFilter(options.apiPredicateConfig)
321 
322         createReportFile(
323             progressTracker,
324             codebase,
325             apiFile,
326             "removed API",
327             options.deleteEmptyRemovedSignatures
328         ) { printWriter ->
329             SignatureWriter(
330                 printWriter,
331                 removedEmit,
332                 removedReference,
333                 false,
334                 options.includeSignatureFormatVersionRemoved,
335                 options.signatureFileFormat,
336                 options.showUnannotated,
337                 options.apiVisitorConfig,
338             )
339         }
340     }
341 
342     val apiPredicateConfigIgnoreShown = options.apiPredicateConfig.copy(ignoreShown = true)
343     val apiReferenceIgnoreShown = ApiPredicate(config = apiPredicateConfigIgnoreShown)
344     options.dexApiFile?.let { apiFile ->
345         val apiFilter = FilterPredicate(ApiPredicate())
346 
347         createReportFile(progressTracker, codebase, apiFile, "DEX API") { printWriter ->
348             DexApiWriter(printWriter, apiFilter, apiReferenceIgnoreShown, options.apiVisitorConfig)
349         }
350     }
351 
352     options.proguard?.let { proguard ->
353         val apiEmit = FilterPredicate(ApiPredicate())
354         createReportFile(progressTracker, codebase, proguard, "Proguard file") { printWriter ->
355             ProguardWriter(printWriter, apiEmit, apiReferenceIgnoreShown)
356         }
357     }
358 
359     options.sdkValueDir?.let { dir ->
360         dir.mkdirs()
361         SdkFileWriter(codebase, dir).generate()
362     }
363 
364     for (check in options.compatibilityChecks) {
365         actionContext.checkCompatibility(signatureFileCache, classResolverProvider, codebase, check)
366     }
367 
368     val previousApiFile = options.migrateNullsFrom
369     if (previousApiFile != null) {
370         val previous =
371             if (previousApiFile.path.endsWith(DOT_JAR)) {
372                 actionContext.loadFromJarFile(previousApiFile)
373             } else {
374                 signatureFileCache.load(signatureFile = SignatureFile.fromFile(previousApiFile))
375             }
376 
377         // If configured, checks for newly added nullness information compared
378         // to the previous stable API and marks the newly annotated elements
379         // as migrated (which will cause the Kotlin compiler to treat problems
380         // as warnings instead of errors
381 
382         NullnessMigration.migrateNulls(codebase, previous)
383 
384         previous.dispose()
385     }
386 
387     convertToWarningNullabilityAnnotations(
388         codebase,
389         options.forceConvertToWarningNullabilityAnnotations
390     )
391 
392     // Now that we've migrated nullness information we can proceed to write non-doc stubs, if any.
393 
394     options.stubsDir?.let {
395         createStubFiles(
396             progressTracker,
397             it,
398             codebase,
399             docStubs = false,
400         )
401     }
402 
403     options.externalAnnotations?.let { extractAnnotations(progressTracker, codebase, it) }
404 
405     val packageCount = codebase.size()
406     progressTracker.progress(
407         "$PROGRAM_NAME finished handling $packageCount packages in ${stopwatch.elapsed(SECONDS)} seconds\n"
408     )
409 }
410 
411 /**
412  * When generating stubs from text signature files some additional items are needed.
413  *
414  * Those items are:
415  * * Constructors - in the signature file a missing constructor means no publicly visible
416  *   constructor but the stub classes still need a constructor.
417  */
418 @Suppress("DEPRECATION")
addMissingItemsRequiredForGeneratingStubsnull419 private fun addMissingItemsRequiredForGeneratingStubs(
420     sourceParser: SourceParser,
421     codebase: Codebase,
422     reporterApiLint: Reporter,
423 ) {
424     // Reuse the existing ApiAnalyzer support for adding constructors that is used in
425     // [loadFromSources], to make sure that the constructors are correct when generating stubs
426     // from source files.
427     val analyzer = ApiAnalyzer(sourceParser, codebase, reporterApiLint, options.apiAnalyzerConfig)
428     analyzer.addConstructors { _ -> true }
429 }
430 
subtractApinull431 private fun ActionContext.subtractApi(
432     signatureFileCache: SignatureFileCache,
433     codebase: Codebase,
434     subtractApiFile: File,
435 ) {
436     val path = subtractApiFile.path
437     val oldCodebase =
438         when {
439             path.endsWith(DOT_TXT) ->
440                 signatureFileCache.load(SignatureFile.fromFile(subtractApiFile))
441             path.endsWith(DOT_JAR) -> loadFromJarFile(subtractApiFile)
442             else ->
443                 throw MetalavaCliException(
444                     "Unsupported $ARG_SUBTRACT_API format, expected .txt or .jar: ${subtractApiFile.name}"
445                 )
446         }
447 
448     @Suppress("DEPRECATION")
449     CodebaseComparator(
450             apiVisitorConfig = @Suppress("DEPRECATION") options.apiVisitorConfig,
451         )
452         .compare(
453             object : ComparisonVisitor() {
454                 override fun compare(old: ClassItem, new: ClassItem) {
455                     new.emit = false
456                 }
457             },
458             oldCodebase,
459             codebase,
460             ApiType.ALL.getReferenceFilter(options.apiPredicateConfig)
461         )
462 }
463 
464 /** Checks compatibility of the given codebase with the codebase described in the signature file. */
465 @Suppress("DEPRECATION")
checkCompatibilitynull466 private fun ActionContext.checkCompatibility(
467     signatureFileCache: SignatureFileCache,
468     classResolverProvider: ClassResolverProvider,
469     newCodebase: Codebase,
470     check: CheckRequest,
471 ) {
472     progressTracker.progress("Checking API compatibility ($check): ")
473 
474     val apiType = check.apiType
475     val generatedApiFile =
476         when (apiType) {
477             ApiType.PUBLIC_API -> options.apiFile
478             ApiType.REMOVED -> options.removedApiFile
479             else -> error("unsupported $apiType")
480         }
481 
482     // Fast path: if we've already generated a signature file, and it's identical to the previously
483     // released API then we're good.
484     //
485     // Reading two files that may be a couple of MBs each isn't a particularly fast path so check
486     // the lengths first and then compare contents byte for byte so that it exits quickly if they're
487     // different and does not do all the UTF-8 conversions.
488     generatedApiFile?.let { apiFile ->
489         val compatibilityCheckCanBeSkipped =
490             check.lastSignatureFile?.let { signatureFile ->
491                 compareFileContents(apiFile, signatureFile)
492             }
493                 ?: false
494         // TODO(b/301282006): Remove global variable use when this can be tested properly
495         fastPathCheckResult = compatibilityCheckCanBeSkipped
496         if (compatibilityCheckCanBeSkipped) return
497     }
498 
499     val oldCodebase =
500         check.previouslyReleasedApi.load(
501             jarLoader = { jarFile -> loadFromJarFile(jarFile) },
502             signatureFileLoader = { signatureFiles ->
503                 signatureFileCache.load(signatureFiles, classResolverProvider.classResolver)
504             }
505         )
506 
507     var baseApi: Codebase? = null
508 
509     if (options.showUnannotated && apiType == ApiType.PUBLIC_API) {
510         val baseApiFile = options.baseApiForCompatCheck
511         if (baseApiFile != null) {
512             baseApi = signatureFileCache.load(signatureFile = SignatureFile.fromFile(baseApiFile))
513         }
514     } else if (options.baseApiForCompatCheck != null) {
515         // This option does not make sense with showAnnotation, as the "base" in that case
516         // is the non-annotated APIs.
517         throw MetalavaCliException(
518             "$ARG_CHECK_COMPATIBILITY_BASE_API is not compatible with --showAnnotation."
519         )
520     }
521 
522     // Wrap the old Codebase in a [MergedCodebase].
523     val mergedOldCodebases = MergedCodebase(listOf(oldCodebase))
524 
525     // If configured, compares the new API with the previous API and reports
526     // any incompatibilities.
527     CompatibilityCheck.checkCompatibility(
528         newCodebase,
529         mergedOldCodebases,
530         apiType,
531         baseApi,
532         options.reporterCompatibilityReleased,
533         options.issueConfiguration,
534     )
535 }
536 
537 /** Compare two files to see if they are byte for byte identical. */
compareFileContentsnull538 private fun compareFileContents(file1: File, file2: File): Boolean {
539     // First check the lengths, if they are different they cannot be identical.
540     if (file1.length() == file2.length()) {
541         // Then load the contents in chunks to see if they differ.
542         file1.inputStream().buffered().use { stream1 ->
543             file2.inputStream().buffered().use { stream2 ->
544                 val buffer1 = ByteArray(DEFAULT_BUFFER_SIZE)
545                 val buffer2 = ByteArray(DEFAULT_BUFFER_SIZE)
546                 do {
547                     val c1 = stream1.read(buffer1)
548                     val c2 = stream2.read(buffer2)
549                     if (c1 != c2) {
550                         // This should never happen as the files are the same length.
551                         break
552                     }
553                     if (c1 == -1) {
554                         // They have both reached the end of file.
555                         return true
556                     }
557                     // Check the buffer contents, if they differ exit the loop otherwise, continue
558                     // on to read the next chunks.
559                 } while (Arrays.equals(buffer1, 0, c1, buffer2, 0, c2))
560             }
561         }
562     }
563     return false
564 }
565 
566 /**
567  * Used to store whether the fast path check in the previous method succeeded or not that can be
568  * checked by tests.
569  *
570  * The test must initialize it to `null`. Then if the fast path check is run it will set it a
571  * non-null to indicate whether the fast path was taken or not. The test can then differentiate
572  * between the following states:
573  * * `null` - the fast path check was not performed.
574  * * `false` - the fast path check was performed and the fast path was not taken.
575  * * `true` - the fast path check was performed and the fast path was taken.
576  *
577  * This is used because there is no nice way to test this code in isolation but the code needs to be
578  * updated to deal with some test failures. This is a hack to avoid a catch-22 where this code needs
579  * to be refactored to allow it to be tested but it needs to be tested before it can be safely
580  * refactored.
581  *
582  * TODO(b/301282006): Remove this variable when the fast path this can be tested properly
583  */
584 internal var fastPathCheckResult: Boolean? = null
585 
convertToWarningNullabilityAnnotationsnull586 private fun convertToWarningNullabilityAnnotations(codebase: Codebase, filter: PackageFilter?) {
587     if (filter != null) {
588         // Our caller has asked for these APIs to not trigger nullness errors (only warnings) if
589         // their callers make incorrect nullness assumptions (for example, calling a function on a
590         // reference of nullable type). The way to communicate this to kotlinc is to mark these
591         // APIs as RecentlyNullable/RecentlyNonNull
592         codebase.accept(MarkPackagesAsRecent(filter))
593     }
594 }
595 
596 @Suppress("DEPRECATION")
loadFromSourcesnull597 private fun ActionContext.loadFromSources(
598     signatureFileCache: SignatureFileCache,
599     classResolverProvider: ClassResolverProvider,
600 ): Codebase {
601     progressTracker.progress("Processing sources: ")
602 
603     val sourceSet =
604         if (options.sources.isEmpty()) {
605             if (options.verbose) {
606                 options.stdout.println(
607                     "No source files specified: recursively including all sources found in the source path (${options.sourcePath.joinToString()}})"
608                 )
609             }
610             SourceSet.createFromSourcePath(options.reporter, options.sourcePath)
611         } else {
612             SourceSet(options.sources, options.sourcePath)
613         }
614 
615     val commonSourceSet =
616         if (options.commonSourcePath.isNotEmpty())
617             SourceSet.createFromSourcePath(options.reporter, options.commonSourcePath)
618         else SourceSet.empty()
619 
620     progressTracker.progress("Reading Codebase: ")
621     val codebase =
622         sourceParser.parseSources(
623             sourceSet,
624             commonSourceSet,
625             "Codebase loaded from source folders",
626             classPath = options.classpath,
627         )
628 
629     progressTracker.progress("Analyzing API: ")
630 
631     val analyzer = ApiAnalyzer(sourceParser, codebase, reporterApiLint, options.apiAnalyzerConfig)
632     analyzer.mergeExternalInclusionAnnotations()
633 
634     analyzer.computeApi()
635 
636     val apiPredicateConfigIgnoreShown = options.apiPredicateConfig.copy(ignoreShown = true)
637     val filterEmit = ApiPredicate(ignoreRemoved = false, config = apiPredicateConfigIgnoreShown)
638     val apiEmitAndReference = ApiPredicate(config = apiPredicateConfigIgnoreShown)
639 
640     // Copy methods from soon-to-be-hidden parents into descendant classes, when necessary. Do
641     // this before merging annotations or performing checks on the API to ensure that these methods
642     // can have annotations added and are checked properly.
643     progressTracker.progress("Insert missing stubs methods: ")
644     analyzer.generateInheritedStubs(apiEmitAndReference, apiEmitAndReference)
645 
646     analyzer.mergeExternalQualifierAnnotations()
647     options.nullabilityAnnotationsValidator?.validateAllFrom(
648         codebase,
649         options.validateNullabilityFromList
650     )
651     options.nullabilityAnnotationsValidator?.report()
652     analyzer.handleStripping()
653 
654     // General API checks for Android APIs
655     AndroidApiChecks(reporterApiLint).check(codebase)
656 
657     options.apiLintOptions.let { apiLintOptions ->
658         if (!apiLintOptions.apiLintEnabled) return@let
659 
660         progressTracker.progress("API Lint: ")
661         val localTimer = Stopwatch.createStarted()
662 
663         // See if we should provide a previous codebase to provide a delta from?
664         val previouslyReleasedApi =
665             apiLintOptions.previouslyReleasedApi?.load(
666                 jarLoader = { jarFile -> loadFromJarFile(jarFile) },
667                 signatureFileLoader = { signatureFiles ->
668                     signatureFileCache.load(signatureFiles, classResolverProvider.classResolver)
669                 }
670             )
671 
672         val apiLintReporter = reporterApiLint as DefaultReporter
673         ApiLint.check(
674             codebase,
675             previouslyReleasedApi,
676             apiLintReporter,
677             options.manifest,
678             options.apiVisitorConfig,
679         )
680         progressTracker.progress(
681             "$PROGRAM_NAME ran api-lint in ${localTimer.elapsed(SECONDS)} seconds with ${apiLintReporter.getBaselineDescription()}"
682         )
683     }
684 
685     // Compute default constructors (and add missing package private constructors
686     // to make stubs compilable if necessary). Do this after all the checks as
687     // these are not part of the API.
688     if (options.stubsDir != null || options.docStubsDir != null) {
689         progressTracker.progress("Insert missing constructors: ")
690         analyzer.addConstructors(filterEmit)
691     }
692 
693     progressTracker.progress("Performing misc API checks: ")
694     analyzer.performChecks()
695 
696     return codebase
697 }
698 
699 /**
700  * Avoids creating a [ClassResolver] unnecessarily as it is expensive to create but once created
701  * allows it to be reused for the same reason.
702  */
703 private class ClassResolverProvider(
704     private val sourceParser: SourceParser,
705     private val apiClassResolution: ApiClassResolution,
706     private val classpath: List<File>
707 ) {
<lambda>null708     val classResolver: ClassResolver? by lazy {
709         if (apiClassResolution == ApiClassResolution.API_CLASSPATH && classpath.isNotEmpty()) {
710             sourceParser.getClassResolver(classpath)
711         } else {
712             null
713         }
714     }
715 }
716 
ActionContextnull717 fun ActionContext.loadFromJarFile(
718     apiJar: File,
719     apiAnalyzerConfig: ApiAnalyzer.Config = @Suppress("DEPRECATION") options.apiAnalyzerConfig,
720 ): Codebase {
721     val jarCodebaseLoader =
722         JarCodebaseLoader.createForSourceParser(
723             progressTracker,
724             reporterApiLint,
725             sourceParser,
726         )
727     return jarCodebaseLoader.loadFromJarFile(apiJar, apiAnalyzerConfig)
728 }
729 
730 @Suppress("DEPRECATION")
extractAnnotationsnull731 private fun extractAnnotations(progressTracker: ProgressTracker, codebase: Codebase, file: File) {
732     val localTimer = Stopwatch.createStarted()
733 
734     options.externalAnnotations?.let { outputFile ->
735         ExtractAnnotations(codebase, options.reporter, outputFile).extractAnnotations()
736         if (options.verbose) {
737             progressTracker.progress(
738                 "$PROGRAM_NAME extracted annotations into $file in ${localTimer.elapsed(SECONDS)} seconds\n"
739             )
740         }
741     }
742 }
743 
744 @Suppress("DEPRECATION")
createStubFilesnull745 private fun createStubFiles(
746     progressTracker: ProgressTracker,
747     stubDir: File,
748     codebase: Codebase,
749     docStubs: Boolean,
750 ) {
751     if (docStubs) {
752         progressTracker.progress("Generating documentation stub files: ")
753     } else {
754         progressTracker.progress("Generating stub files: ")
755     }
756 
757     val localTimer = Stopwatch.createStarted()
758 
759     val stubWriterConfig =
760         options.stubWriterConfig.let {
761             if (docStubs) {
762                 // Doc stubs always include documentation.
763                 it.copy(includeDocumentationInStubs = true)
764             } else {
765                 it
766             }
767         }
768 
769     val stubWriter =
770         StubWriter(
771             stubsDir = stubDir,
772             generateAnnotations = options.generateAnnotations,
773             preFiltered = codebase.preFiltered,
774             docStubs = docStubs,
775             reporter = options.reporter,
776             config = stubWriterConfig,
777         )
778     codebase.accept(stubWriter)
779 
780     if (docStubs) {
781         // Overview docs? These are generally in the empty package.
782         codebase.findPackage("")?.let { empty ->
783             val overview = empty.overviewDocumentation
784             if (!overview.isNullOrBlank()) {
785                 stubWriter.writeDocOverview(empty, overview)
786             }
787         }
788     }
789 
790     progressTracker.progress(
791         "$PROGRAM_NAME wrote ${if (docStubs) "documentation" else ""} stubs directory $stubDir in ${
792         localTimer.elapsed(SECONDS)} seconds\n"
793     )
794 }
795 
796 @Suppress("DEPRECATION")
createReportFilenull797 fun createReportFile(
798     progressTracker: ProgressTracker,
799     codebase: Codebase,
800     apiFile: File,
801     description: String?,
802     deleteEmptyFiles: Boolean = false,
803     createVisitor: (PrintWriter) -> ItemVisitor
804 ) {
805     if (description != null) {
806         progressTracker.progress("Writing $description file: ")
807     }
808     val localTimer = Stopwatch.createStarted()
809     try {
810         val stringWriter = StringWriter()
811         val writer = PrintWriter(stringWriter)
812         writer.use { printWriter ->
813             val apiWriter = createVisitor(printWriter)
814             codebase.accept(apiWriter)
815         }
816         val text = stringWriter.toString()
817         if (text.isNotEmpty() || !deleteEmptyFiles) {
818             apiFile.writeText(text)
819         }
820     } catch (e: IOException) {
821         options.reporter.report(Issues.IO_ERROR, apiFile, "Cannot open file for write.")
822     }
823     if (description != null) {
824         progressTracker.progress(
825             "$PROGRAM_NAME wrote $description file $apiFile in ${localTimer.elapsed(SECONDS)} seconds\n"
826         )
827     }
828 }
829 
createMetalavaCommandnull830 private fun createMetalavaCommand(
831     executionEnvironment: ExecutionEnvironment,
832     progressTracker: ProgressTracker
833 ): MetalavaCommand {
834     val command =
835         MetalavaCommand(
836             executionEnvironment = executionEnvironment,
837             progressTracker = progressTracker,
838             defaultCommandName = "main",
839         )
840     command.subcommands(
841         MainCommand(command.commonOptions, executionEnvironment),
842         AndroidJarsToSignaturesCommand(),
843         HelpCommand(),
844         JarToJDiffCommand(),
845         MakeAnnotationsPackagePrivateCommand(),
846         MergeSignaturesCommand(),
847         SignatureToDexCommand(),
848         SignatureToJDiffCommand(),
849         UpdateSignatureHeaderCommand(),
850         VersionCommand(),
851     )
852     return command
853 }
854