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