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