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