blob: 5b610d607efa88f2ff1961b654110923f11d1573 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:JvmName("Driver")
package com.android.tools.metalava
import com.android.SdkConstants.DOT_JAR
import com.android.SdkConstants.DOT_TXT
import com.android.tools.lint.detector.api.assertionsEnabled
import com.android.tools.metalava.CompatibilityCheck.CheckRequest
import com.android.tools.metalava.apilevels.ApiGenerator
import com.android.tools.metalava.cli.common.CommonOptions
import com.android.tools.metalava.cli.common.EarlyOptions
import com.android.tools.metalava.cli.common.MetalavaCliException
import com.android.tools.metalava.cli.common.MetalavaCommand
import com.android.tools.metalava.cli.common.MetalavaLocalization
import com.android.tools.metalava.cli.common.ReporterOptions
import com.android.tools.metalava.cli.common.VersionCommand
import com.android.tools.metalava.cli.common.registerPostCommandAction
import com.android.tools.metalava.cli.common.stderr
import com.android.tools.metalava.cli.common.stdout
import com.android.tools.metalava.cli.help.HelpCommand
import com.android.tools.metalava.cli.internal.MakeAnnotationsPackagePrivateCommand
import com.android.tools.metalava.cli.signature.MergeSignaturesCommand
import com.android.tools.metalava.cli.signature.SignatureFormatOptions
import com.android.tools.metalava.cli.signature.UpdateSignatureHeaderCommand
import com.android.tools.metalava.model.ClassItem
import com.android.tools.metalava.model.ClassResolver
import com.android.tools.metalava.model.Codebase
import com.android.tools.metalava.model.Item
import com.android.tools.metalava.model.psi.gatherSources
import com.android.tools.metalava.model.source.EnvironmentManager
import com.android.tools.metalava.model.source.SourceModelProvider
import com.android.tools.metalava.model.source.SourceParser
import com.android.tools.metalava.model.text.ApiClassResolution
import com.android.tools.metalava.model.text.TextClassItem
import com.android.tools.metalava.model.text.TextCodebase
import com.android.tools.metalava.model.text.TextMethodItem
import com.android.tools.metalava.model.visitors.ApiVisitor
import com.android.tools.metalava.reporter.Issues
import com.android.tools.metalava.reporter.Reporter
import com.android.tools.metalava.stub.StubWriter
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.context
import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.groups.OptionGroup
import com.github.ajalt.clikt.parameters.groups.provideDelegate
import com.google.common.base.Stopwatch
import java.io.File
import java.io.IOException
import java.io.OutputStreamWriter
import java.io.PrintWriter
import java.io.StringWriter
import java.util.concurrent.TimeUnit.SECONDS
import java.util.function.Predicate
import kotlin.system.exitProcess
import kotlin.text.Charsets.UTF_8
const val PROGRAM_NAME = "metalava"
fun main(args: Array<String>) {
val stdout = PrintWriter(OutputStreamWriter(System.out))
val stderr = PrintWriter(OutputStreamWriter(System.err))
val exitCode = run(args, stdout, stderr)
stdout.flush()
stderr.flush()
exitProcess(exitCode)
}
/**
* The metadata driver is a command line interface to extracting various metadata from a source tree
* (or existing signature files etc.). Run with --help to see more details.
*/
fun run(
originalArgs: Array<String>,
stdout: PrintWriter,
stderr: PrintWriter,
): Int {
// Preprocess the arguments by adding any additional arguments specified in environment
// variables.
val modifiedArgs = preprocessArgv(originalArgs)
// Process the early options. This does not consume any arguments, they will be parsed again
// later. A little inefficient but produces cleaner code.
val earlyOptions = EarlyOptions.parse(modifiedArgs)
val progressTracker = ProgressTracker(earlyOptions.verbosity.verbose, stdout)
progressTracker.progress("$PROGRAM_NAME started\n")
// Dump the arguments, and maybe generate a rerun-script.
maybeDumpArgv(stdout, originalArgs, modifiedArgs)
// Actual work begins here.
val command =
createMetalavaCommand(
stdout,
stderr,
progressTracker,
)
val exitCode = command.process(modifiedArgs)
stdout.flush()
stderr.flush()
progressTracker.progress("$PROGRAM_NAME exiting with exit code $exitCode\n")
return exitCode
}
private fun repeatErrors(writer: PrintWriter, reporters: List<DefaultReporter>, max: Int) {
writer.println("Error: $PROGRAM_NAME detected the following problems:")
val totalErrors = reporters.sumOf { it.errorCount }
var remainingCap = max
var totalShown = 0
reporters.forEach {
val numShown = it.printErrors(writer, remainingCap)
remainingCap -= numShown
totalShown += numShown
}
if (totalShown < totalErrors) {
writer.println(
"${totalErrors - totalShown} more error(s) omitted. Search the log for 'error:' to find all of them."
)
}
}
@Suppress("DEPRECATION")
internal fun processFlags(
environmentManager: EnvironmentManager,
progressTracker: ProgressTracker
) {
val stopwatch = Stopwatch.createStarted()
val reporter = options.reporter
val sourceParser =
environmentManager.createSourceParser(
reporter = reporter,
annotationManager = options.annotationManager,
javaLanguageLevel = options.javaLanguageLevelAsString,
kotlinLanguageLevel = options.kotlinLanguageLevelAsString,
useK2Uast = options.useK2Uast,
jdkHome = options.jdkHome,
)
val sources = options.sources
val codebase =
if (sources.isNotEmpty() && sources[0].path.endsWith(DOT_TXT)) {
// Make sure all the source files have .txt extensions.
sources
.firstOrNull { !it.path.endsWith(DOT_TXT) }
?.let {
throw MetalavaCliException(
"Inconsistent input file types: The first file is of $DOT_TXT, but detected different extension in ${it.path}"
)
}
val classResolver = getClassResolver(sourceParser)
val textCodebase = SignatureFileLoader.loadFiles(sources, classResolver)
// If this codebase was loaded in order to generate stubs then they will need some
// additional items to be added that were purposely removed from the signature files.
if (options.stubsDir != null) {
addMissingItemsRequiredForGeneratingStubs(sourceParser, textCodebase)
}
textCodebase
} else if (options.apiJar != null) {
loadFromJarFile(progressTracker, reporter, sourceParser, options.apiJar!!)
} else if (sources.size == 1 && sources[0].path.endsWith(DOT_JAR)) {
loadFromJarFile(progressTracker, reporter, sourceParser, sources[0])
} else if (sources.isNotEmpty() || options.sourcePath.isNotEmpty()) {
loadFromSources(progressTracker, reporter, sourceParser)
} else {
return
}
progressTracker.progress(
"$PROGRAM_NAME analyzed API in ${stopwatch.elapsed(SECONDS)} seconds\n"
)
options.subtractApi?.let {
progressTracker.progress("Subtracting API: ")
subtractApi(progressTracker, reporter, sourceParser, codebase, it)
}
if (options.hideAnnotations.matchesAnnotationName(ANDROID_FLAGGED_API)) {
reallyHideFlaggedSystemApis(codebase)
}
val androidApiLevelXml = options.generateApiLevelXml
val apiLevelJars = options.apiLevelJars
if (androidApiLevelXml != null && apiLevelJars != null) {
assert(options.currentApiLevel != -1)
progressTracker.progress(
"Generating API levels XML descriptor file, ${androidApiLevelXml.name}: "
)
ApiGenerator.generateXml(
apiLevelJars,
options.firstApiLevel,
options.currentApiLevel,
options.isDeveloperPreviewBuild(),
androidApiLevelXml,
codebase,
options.sdkJarRoot,
options.sdkInfoFile,
options.removeMissingClassesInApiLevels
)
}
if (options.docStubsDir != null || options.enhanceDocumentation) {
if (!codebase.supportsDocumentation()) {
error("Codebase does not support documentation, so it cannot be enhanced.")
}
progressTracker.progress("Enhancing docs: ")
val docAnalyzer = DocAnalyzer(codebase, reporter)
docAnalyzer.enhance()
val applyApiLevelsXml = options.applyApiLevelsXml
if (applyApiLevelsXml != null) {
progressTracker.progress("Applying API levels")
docAnalyzer.applyApiLevels(applyApiLevelsXml)
}
}
val apiVersionsJson = options.generateApiVersionsJson
val apiVersionNames = options.apiVersionNames
if (apiVersionsJson != null && apiVersionNames != null) {
progressTracker.progress(
"Generating API version history JSON file, ${apiVersionsJson.name}: "
)
ApiGenerator.generateJson(
// The signature files can be null if the current version is the only version
options.apiVersionSignatureFiles ?: emptyList(),
codebase,
apiVersionsJson,
apiVersionNames
)
}
// Generate the documentation stubs *before* we migrate nullness information.
options.docStubsDir?.let {
createStubFiles(
progressTracker,
it,
codebase,
docStubs = true,
writeStubList = options.docStubsSourceList != null
)
}
// Based on the input flags, generates various output files such
// as signature files and/or stubs files
options.apiFile?.let { apiFile ->
val apiType = ApiType.PUBLIC_API
val apiEmit = apiType.getEmitFilter(options.apiPredicateConfig)
val apiReference = apiType.getReferenceFilter(options.apiPredicateConfig)
createReportFile(progressTracker, codebase, apiFile, "API") { printWriter ->
SignatureWriter(
printWriter,
apiEmit,
apiReference,
codebase.preFiltered,
fileFormat = options.signatureFileFormat,
showUnannotated = options.showUnannotated,
apiVisitorConfig = options.apiVisitorConfig,
)
}
}
options.apiXmlFile?.let { apiFile ->
val apiType = ApiType.PUBLIC_API
val apiEmit = apiType.getEmitFilter(options.apiPredicateConfig)
val apiReference = apiType.getReferenceFilter(options.apiPredicateConfig)
createReportFile(progressTracker, codebase, apiFile, "XML API") { printWriter ->
JDiffXmlWriter(
printWriter,
apiEmit,
apiReference,
codebase.preFiltered,
showUnannotated = @Suppress("DEPRECATION") options.showUnannotated,
config = options.apiVisitorConfig,
)
}
}
options.removedApiFile?.let { apiFile ->
val unfiltered = codebase.original ?: codebase
val apiType = ApiType.REMOVED
val removedEmit = apiType.getEmitFilter(options.apiPredicateConfig)
val removedReference = apiType.getReferenceFilter(options.apiPredicateConfig)
createReportFile(
progressTracker,
unfiltered,
apiFile,
"removed API",
options.deleteEmptyRemovedSignatures
) { printWriter ->
SignatureWriter(
printWriter,
removedEmit,
removedReference,
codebase.original != null,
options.includeSignatureFormatVersionRemoved,
options.signatureFileFormat,
options.showUnannotated,
options.apiVisitorConfig,
)
}
}
val apiPredicateConfigIgnoreShown = options.apiPredicateConfig.copy(ignoreShown = true)
val apiReferenceIgnoreShown = ApiPredicate(config = apiPredicateConfigIgnoreShown)
options.dexApiFile?.let { apiFile ->
val apiFilter = FilterPredicate(ApiPredicate())
val memberIsNotCloned: Predicate<Item> = Predicate { !it.isCloned() }
val dexApiEmit = memberIsNotCloned.and(apiFilter)
createReportFile(progressTracker, codebase, apiFile, "DEX API") { printWriter ->
DexApiWriter(printWriter, dexApiEmit, apiReferenceIgnoreShown)
}
}
options.proguard?.let { proguard ->
val apiEmit = FilterPredicate(ApiPredicate())
createReportFile(progressTracker, codebase, proguard, "Proguard file") { printWriter ->
ProguardWriter(printWriter, apiEmit, apiReferenceIgnoreShown)
}
}
options.sdkValueDir?.let { dir ->
dir.mkdirs()
SdkFileWriter(codebase, dir).generate()
}
for (check in options.compatibilityChecks) {
checkCompatibility(progressTracker, reporter, sourceParser, codebase, check)
}
val previousApiFile = options.migrateNullsFrom
if (previousApiFile != null) {
val previous =
if (previousApiFile.path.endsWith(DOT_JAR)) {
loadFromJarFile(progressTracker, reporter, sourceParser, previousApiFile)
} else {
SignatureFileLoader.load(file = previousApiFile)
}
// If configured, checks for newly added nullness information compared
// to the previous stable API and marks the newly annotated elements
// as migrated (which will cause the Kotlin compiler to treat problems
// as warnings instead of errors
NullnessMigration.migrateNulls(codebase, previous)
previous.dispose()
}
convertToWarningNullabilityAnnotations(
codebase,
options.forceConvertToWarningNullabilityAnnotations
)
// Now that we've migrated nullness information we can proceed to write non-doc stubs, if any.
options.stubsDir?.let {
createStubFiles(
progressTracker,
it,
codebase,
docStubs = false,
writeStubList = options.stubsSourceList != null
)
}
if (options.docStubsDir == null && options.stubsDir == null) {
val writeStubsFile: (File) -> Unit = { file ->
val root = File("").absoluteFile
val rootPath = root.path
val contents =
sources.joinToString(" ") {
val path = it.path
if (path.startsWith(rootPath)) {
path.substring(rootPath.length)
} else {
path
}
}
file.writeText(contents)
}
options.stubsSourceList?.let(writeStubsFile)
options.docStubsSourceList?.let(writeStubsFile)
}
options.externalAnnotations?.let { extractAnnotations(progressTracker, codebase, it) }
val packageCount = codebase.size()
progressTracker.progress(
"$PROGRAM_NAME finished handling $packageCount packages in ${stopwatch.elapsed(SECONDS)} seconds\n"
)
}
/**
* When generate stubs from text signature files some additional items are needed.
*
* Those items are:
* * Constructors - in the signature file a missing constructor means no publicly visible
* constructor but the stub classes still need a constructor.
* * Concrete methods - in the signature file concrete implementations of inherited abstract methods
* are not listed on concrete classes but the stub concrete classes need those implementations.
*/
@Suppress("DEPRECATION")
private fun addMissingItemsRequiredForGeneratingStubs(
sourceParser: SourceParser,
textCodebase: TextCodebase,
) {
// Only add constructors if the codebase does not fall back to loading classes from the
// classpath. This is needed because only the TextCodebase supports adding constructors
// in this way.
if (options.apiClassResolution == ApiClassResolution.API) {
// Reuse the existing ApiAnalyzer support for adding constructors that is used in
// [loadFromSources], to make sure that the constructors are correct when generating stubs
// from source files.
val analyzer =
ApiAnalyzer(sourceParser, textCodebase, options.reporter, options.apiAnalyzerConfig)
analyzer.addConstructors { _ -> true }
addMissingConcreteMethods(
textCodebase.getPackages().allClasses().map { it as TextClassItem }.toList()
)
}
}
/**
* Add concrete implementations of inherited abstract methods to non-abstract class when generating
* from-text stubs. Iterate through the hierarchy and collect all super abstract methods that need
* to be added. These are not included in the signature files but omitting these methods will lead
* to compile error.
*/
fun addMissingConcreteMethods(allClasses: List<TextClassItem>) {
for (cl in allClasses) {
// If class is interface, naively iterate through all parent class and interfaces
// and resolve inheritance of override equivalent signatures
// Find intersection of super class/interface default methods
// Resolve conflict by adding signature
// https://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.4.1.3
if (cl.isInterface()) {
// We only need to track one method item(value) with the signature(key),
// since the containing class does not matter if a method to be added is found
// as method.duplicate(cl) sets containing class to cl.
// Therefore, the value of methodMap can be overwritten.
val methodMap = mutableMapOf<String, TextMethodItem>()
val methodCount = mutableMapOf<String, Int>()
val hasDefault = mutableMapOf<String, Boolean>()
for (superInterfaceOrClass in cl.getParentAndInterfaces()) {
val methods = superInterfaceOrClass.methods().map { it as TextMethodItem }
for (method in methods) {
val signature = method.toSignatureString()
val isDefault = method.modifiers.isDefault()
val newCount = methodCount.getOrDefault(signature, 0) + 1
val newHasDefault = hasDefault.getOrDefault(signature, false) || isDefault
methodMap[signature] = method
methodCount[signature] = newCount
hasDefault[signature] = newHasDefault
// If the method has appeared more than once, there may be a potential
// conflict
// thus add the method to the interface
if (
newHasDefault && newCount == 2 && !cl.containsMethodInClassContext(method)
) {
val m = method.duplicate(cl) as TextMethodItem
m.modifiers.setAbstract(true)
m.modifiers.setDefault(false)
cl.addMethod(m)
}
}
}
}
// If class is a concrete class, iterate through all hierarchy and
// find all missing abstract methods.
// Only add methods that are not implemented in the hierarchy and not included
else if (!cl.isAbstractClass() && !cl.isEnum()) {
val superMethodsToBeOverridden = mutableListOf<TextMethodItem>()
val hierarchyClassesList = cl.getAllSuperClassesAndInterfaces().toMutableList()
while (hierarchyClassesList.isNotEmpty()) {
val ancestorClass = hierarchyClassesList.removeLast()
val abstractMethods = ancestorClass.methods().filter { it.modifiers.isAbstract() }
for (method in abstractMethods) {
// We do not compare this against all ancestors of cl,
// because an abstract method cannot be overridden at its ancestor class.
// Thus, we compare against hierarchyClassesList.
if (
hierarchyClassesList.all { !it.containsMethodInClassContext(method) } &&
!cl.containsMethodInClassContext(method)
) {
superMethodsToBeOverridden.add(method as TextMethodItem)
}
}
}
for (superMethod in superMethodsToBeOverridden) {
// MethodItem.duplicate() sets the containing class of
// the duplicated method item as the input parameter.
// Thus, the method items to be overridden are duplicated here after the
// ancestor classes iteration so that the method items are correctly compared.
val m = superMethod.duplicate(cl) as TextMethodItem
m.modifiers.setAbstract(false)
cl.addMethod(m)
}
}
}
}
fun subtractApi(
progressTracker: ProgressTracker,
reporter: Reporter,
sourceParser: SourceParser,
codebase: Codebase,
subtractApiFile: File,
) {
val path = subtractApiFile.path
val oldCodebase =
when {
path.endsWith(DOT_TXT) -> SignatureFileLoader.load(subtractApiFile)
path.endsWith(DOT_JAR) ->
loadFromJarFile(progressTracker, reporter, sourceParser, subtractApiFile)
else ->
throw MetalavaCliException(
"Unsupported $ARG_SUBTRACT_API format, expected .txt or .jar: ${subtractApiFile.name}"
)
}
@Suppress("DEPRECATION")
CodebaseComparator()
.compare(
object : ComparisonVisitor() {
override fun compare(old: ClassItem, new: ClassItem) {
new.emit = false
}
},
oldCodebase,
codebase,
ApiType.ALL.getReferenceFilter(options.apiPredicateConfig)
)
}
fun reallyHideFlaggedSystemApis(codebase: Codebase) {
@Suppress("DEPRECATION")
val apiPredicateConfigIgnoreShown = options.apiPredicateConfig.copy(ignoreShown = true)
val apiEmitAndReference = ApiPredicate(config = apiPredicateConfigIgnoreShown)
codebase.accept(
object :
ApiVisitor(
filterEmit = apiEmitAndReference,
filterReference = apiEmitAndReference,
includeEmptyOuterClasses = true
) {
override fun visitItem(item: Item) {
item.modifiers.findAnnotation(ANDROID_FLAGGED_API) ?: return
item.hidden = true
item.mutableModifiers().removeAnnotations { it.isShowAnnotation() }
}
}
)
}
/** Checks compatibility of the given codebase with the codebase described in the signature file. */
@Suppress("DEPRECATION")
fun checkCompatibility(
progressTracker: ProgressTracker,
reporter: Reporter,
sourceParser: SourceParser,
newCodebase: Codebase,
check: CheckRequest,
) {
progressTracker.progress("Checking API compatibility ($check): ")
val signatureFile = check.file
val oldCodebase =
if (signatureFile.path.endsWith(DOT_JAR)) {
loadFromJarFile(progressTracker, reporter, sourceParser, signatureFile)
} else {
val classResolver = getClassResolver(sourceParser)
SignatureFileLoader.load(signatureFile, classResolver)
}
var baseApi: Codebase? = null
val apiType = check.apiType
if (options.showUnannotated && apiType == ApiType.PUBLIC_API) {
// Fast path: if we've already generated a signature file, and it's identical, we're good!
val apiFile = options.apiFile
if (apiFile != null && apiFile.readText(UTF_8) == signatureFile.readText(UTF_8)) {
return
}
val baseApiFile = options.baseApiForCompatCheck
if (baseApiFile != null) {
baseApi = SignatureFileLoader.load(file = baseApiFile)
}
} else if (options.baseApiForCompatCheck != null) {
// This option does not make sense with showAnnotation, as the "base" in that case
// is the non-annotated APIs.
throw MetalavaCliException(
"$ARG_CHECK_COMPATIBILITY_BASE_API is not compatible with --showAnnotation."
)
}
// If configured, compares the new API with the previous API and reports
// any incompatibilities.
CompatibilityCheck.checkCompatibility(
newCodebase,
oldCodebase,
apiType,
baseApi,
options.reporterCompatibilityReleased,
options.issueConfiguration,
)
}
private fun convertToWarningNullabilityAnnotations(codebase: Codebase, filter: PackageFilter?) {
if (filter != null) {
// Our caller has asked for these APIs to not trigger nullness errors (only warnings) if
// their callers make incorrect nullness assumptions (for example, calling a function on a
// reference of nullable type). The way to communicate this to kotlinc is to mark these
// APIs as RecentlyNullable/RecentlyNonNull
codebase.accept(MarkPackagesAsRecent(filter))
}
}
@Suppress("DEPRECATION")
private fun loadFromSources(
progressTracker: ProgressTracker,
reporter: Reporter,
sourceParser: SourceParser,
): Codebase {
progressTracker.progress("Processing sources: ")
val sources =
options.sources.ifEmpty {
if (options.verbose) {
options.stdout.println(
"No source files specified: recursively including all sources found in the source path (${options.sourcePath.joinToString()}})"
)
}
gatherSources(options.reporter, options.sourcePath)
}
progressTracker.progress("Reading Codebase: ")
val codebase =
sourceParser.parseSources(
sources,
"Codebase loaded from source folders",
sourcePath = options.sourcePath,
classPath = options.classpath,
)
progressTracker.progress("Analyzing API: ")
val analyzer = ApiAnalyzer(sourceParser, codebase, options.reporter, options.apiAnalyzerConfig)
analyzer.mergeExternalInclusionAnnotations()
analyzer.computeApi()
val apiPredicateConfigIgnoreShown = options.apiPredicateConfig.copy(ignoreShown = true)
val filterEmit = ApiPredicate(ignoreRemoved = false, config = apiPredicateConfigIgnoreShown)
val apiEmitAndReference = ApiPredicate(config = apiPredicateConfigIgnoreShown)
// Copy methods from soon-to-be-hidden parents into descendant classes, when necessary. Do
// this before merging annotations or performing checks on the API to ensure that these methods
// can have annotations added and are checked properly.
progressTracker.progress("Insert missing stubs methods: ")
analyzer.generateInheritedStubs(apiEmitAndReference, apiEmitAndReference)
analyzer.mergeExternalQualifierAnnotations()
options.nullabilityAnnotationsValidator?.validateAllFrom(
codebase,
options.validateNullabilityFromList
)
options.nullabilityAnnotationsValidator?.report()
analyzer.handleStripping()
// General API checks for Android APIs
AndroidApiChecks(options.reporter).check(codebase)
if (options.checkApi) {
progressTracker.progress("API Lint: ")
val localTimer = Stopwatch.createStarted()
// See if we should provide a previous codebase to provide a delta from?
val previousApiFile = options.checkApiBaselineApiFile
val previous =
when {
previousApiFile == null -> null
previousApiFile.path.endsWith(DOT_JAR) ->
loadFromJarFile(progressTracker, reporter, sourceParser, previousApiFile)
else -> SignatureFileLoader.load(file = previousApiFile)
}
val apiLintReporter = options.reporterApiLint as DefaultReporter
ApiLint.check(codebase, previous, apiLintReporter)
progressTracker.progress(
"$PROGRAM_NAME ran api-lint in ${localTimer.elapsed(SECONDS)} seconds with ${apiLintReporter.getBaselineDescription()}"
)
}
// Compute default constructors (and add missing package private constructors
// to make stubs compilable if necessary). Do this after all the checks as
// these are not part of the API.
if (options.stubsDir != null || options.docStubsDir != null) {
progressTracker.progress("Insert missing constructors: ")
analyzer.addConstructors(filterEmit)
}
progressTracker.progress("Performing misc API checks: ")
analyzer.performChecks()
return codebase
}
@Suppress("DEPRECATION")
private fun getClassResolver(sourceParser: SourceParser): ClassResolver? {
val apiClassResolution = options.apiClassResolution
val classpath = options.classpath
return if (apiClassResolution == ApiClassResolution.API_CLASSPATH && classpath.isNotEmpty()) {
sourceParser.getClassResolver(classpath)
} else {
null
}
}
@Suppress("DEPRECATION")
fun loadFromJarFile(
progressTracker: ProgressTracker,
reporter: Reporter,
sourceParser: SourceParser,
apiJar: File,
preFiltered: Boolean = false,
apiAnalyzerConfig: ApiAnalyzer.Config = options.apiAnalyzerConfig,
codebaseValidator: (Codebase) -> Unit = { codebase ->
options.nullabilityAnnotationsValidator?.validateAllFrom(
codebase,
options.validateNullabilityFromList
)
options.nullabilityAnnotationsValidator?.report()
},
apiPredicateConfig: ApiPredicate.Config = options.apiPredicateConfig,
): Codebase {
progressTracker.progress("Processing jar file: ")
val codebase = sourceParser.loadFromJar(apiJar, preFiltered)
val apiEmit =
ApiPredicate(
config = apiPredicateConfig.copy(ignoreShown = true),
)
val apiReference = apiEmit
val analyzer = ApiAnalyzer(sourceParser, codebase, reporter, apiAnalyzerConfig)
analyzer.mergeExternalInclusionAnnotations()
analyzer.computeApi()
analyzer.mergeExternalQualifierAnnotations()
codebaseValidator(codebase)
analyzer.generateInheritedStubs(apiEmit, apiReference)
return codebase
}
internal fun disableStderrDumping(): Boolean {
return !assertionsEnabled() &&
System.getenv(ENV_VAR_METALAVA_DUMP_ARGV) == null &&
!isUnderTest()
}
@Suppress("DEPRECATION")
private fun extractAnnotations(progressTracker: ProgressTracker, codebase: Codebase, file: File) {
val localTimer = Stopwatch.createStarted()
options.externalAnnotations?.let { outputFile ->
ExtractAnnotations(codebase, options.reporter, outputFile).extractAnnotations()
if (options.verbose) {
progressTracker.progress(
"$PROGRAM_NAME extracted annotations into $file in ${localTimer.elapsed(SECONDS)} seconds\n"
)
}
}
}
@Suppress("DEPRECATION")
private fun createStubFiles(
progressTracker: ProgressTracker,
stubDir: File,
codebase: Codebase,
docStubs: Boolean,
writeStubList: Boolean
) {
if (codebase is TextCodebase) {
if (options.verbose) {
options.stdout.println(
"Generating stubs from text based codebase is an experimental feature. " +
"It is not guaranteed that stubs generated from text based codebase are " +
"class level equivalent to the stubs generated from source files. "
)
}
}
// Temporary bug workaround for org.chromium.arc
if (options.sourcePath.firstOrNull()?.path?.endsWith("org.chromium.arc") == true) {
codebase.findClass("org.chromium.mojo.bindings.Callbacks")?.hidden = true
}
if (docStubs) {
progressTracker.progress("Generating documentation stub files: ")
} else {
progressTracker.progress("Generating stub files: ")
}
val localTimer = Stopwatch.createStarted()
val stubWriter =
StubWriter(
codebase = codebase,
stubsDir = stubDir,
generateAnnotations = options.generateAnnotations,
preFiltered = codebase.preFiltered,
docStubs = docStubs,
reporter = options.reporter,
)
codebase.accept(stubWriter)
if (docStubs) {
// Overview docs? These are generally in the empty package.
codebase.findPackage("")?.let { empty ->
val overview = codebase.getPackageDocs()?.getOverviewDocumentation(empty)
if (!overview.isNullOrBlank()) {
stubWriter.writeDocOverview(empty, overview)
}
}
}
if (writeStubList) {
// Optionally also write out a list of source files that were generated; used
// for example to point javadoc to the stubs output to generate documentation
val file =
if (docStubs) {
options.docStubsSourceList ?: options.stubsSourceList
} else {
options.stubsSourceList
}
file?.let {
val root = File("").absoluteFile
stubWriter.writeSourceList(it, root)
}
}
progressTracker.progress(
"$PROGRAM_NAME wrote ${if (docStubs) "documentation" else ""} stubs directory $stubDir in ${
localTimer.elapsed(SECONDS)} seconds\n"
)
}
@Suppress("DEPRECATION")
fun createReportFile(
progressTracker: ProgressTracker,
codebase: Codebase,
apiFile: File,
description: String?,
deleteEmptyFiles: Boolean = false,
createVisitor: (PrintWriter) -> ApiVisitor
) {
if (description != null) {
progressTracker.progress("Writing $description file: ")
}
val localTimer = Stopwatch.createStarted()
try {
val stringWriter = StringWriter()
val writer = PrintWriter(stringWriter)
writer.use { printWriter ->
val apiWriter = createVisitor(printWriter)
codebase.accept(apiWriter)
}
val text = stringWriter.toString()
if (text.isNotEmpty() || !deleteEmptyFiles) {
apiFile.writeText(text)
}
} catch (e: IOException) {
options.reporter.report(Issues.IO_ERROR, apiFile, "Cannot open file for write.")
}
if (description != null) {
progressTracker.progress(
"$PROGRAM_NAME wrote $description file $apiFile in ${localTimer.elapsed(SECONDS)} seconds\n"
)
}
}
/** Whether metalava is running unit tests */
fun isUnderTest() = java.lang.Boolean.getBoolean(ENV_VAR_METALAVA_TESTS_RUNNING)
/** Whether metalava is being invoked as part of an Android platform build */
fun isBuildingAndroid() = System.getenv("ANDROID_BUILD_TOP") != null && !isUnderTest()
private fun createMetalavaCommand(
stdout: PrintWriter,
stderr: PrintWriter,
progressTracker: ProgressTracker
): MetalavaCommand {
val command =
MetalavaCommand(
stdout,
stderr,
{ commonOptions ->
DriverCommand(
commonOptions,
progressTracker,
)
},
progressTracker,
OptionsHelp::getUsage,
)
command.subcommands(
AndroidJarsToSignaturesCommand(),
HelpCommand(),
MakeAnnotationsPackagePrivateCommand(),
MergeSignaturesCommand(),
SignatureToJDiffCommand(),
UpdateSignatureHeaderCommand(),
VersionCommand(),
)
return command
}
/**
* A command that is passed to [MetalavaCommand.defaultCommand] when the main metalava functionality
* needs to be run when no subcommand is provided.
*/
private class DriverCommand(
commonOptions: CommonOptions,
private val progressTracker: ProgressTracker,
) : CliktCommand(treatUnknownOptionsAsArgs = true) {
init {
// Although, the `helpFormatter` is inherited from the parent context unless overridden the
// same is not true for the `localization` so make sure to initialize it for this command.
context { localization = MetalavaLocalization() }
}
/**
* Property into which all the arguments (and unknown options) are gathered.
*
* This does not provide any `help` so that it is excluded from the `help` by
* [MetalavaCommand.excludeArgumentsWithNoHelp].
*/
private val flags by argument().multiple()
/** Issue reporter configuration. */
private val reporterOptions by ReporterOptions()
/** Signature file options. */
private val signatureFileOptions by SignatureFileOptions()
/** Signature format options. */
private val signatureFormatOptions by SignatureFormatOptions()
/** Stub generation options. */
private val stubGenerationOptions by StubGenerationOptions()
/**
* Add [Options] (an [OptionGroup]) so that any Clikt defined properties will be processed by
* Clikt.
*/
private val optionGroup by
Options(
commonOptions = commonOptions,
reporterOptions = reporterOptions,
signatureFileOptions = signatureFileOptions,
signatureFormatOptions = signatureFormatOptions,
stubGenerationOptions = stubGenerationOptions,
)
override fun run() {
// Make sure to flush out the baseline files, close files and write any final messages.
registerPostCommandAction {
// Update and close all baseline files.
optionGroup.allBaselines.forEach { baseline ->
if (optionGroup.verbose) {
baseline.dumpStats(optionGroup.stdout)
}
if (baseline.close()) {
if (!optionGroup.quiet) {
stdout.println(
"$PROGRAM_NAME wrote updated baseline to ${baseline.updateFile}"
)
}
}
}
optionGroup.reportEvenIfSuppressedWriter?.close()
// Show failure messages, if any.
optionGroup.allReporters.forEach { it.writeErrorMessage(stderr) }
}
// Get any remaining arguments/options that were not handled by Clikt.
val remainingArgs = flags.toTypedArray()
// Parse any remaining arguments
optionGroup.parse(remainingArgs, stdout, stderr)
// Update the global options.
@Suppress("DEPRECATION")
options = optionGroup
val sourceModelProvider =
SourceModelProvider.getImplementation(optionGroup.sourceModelProvider)
sourceModelProvider.createEnvironmentManager(disableStderrDumping()).use {
processFlags(it, progressTracker)
}
if (optionGroup.allReporters.any { it.hasErrors() } && !optionGroup.passBaselineUpdates) {
// Repeat the errors at the end to make it easy to find the actual problems.
if (reporterOptions.repeatErrorsMax > 0) {
repeatErrors(stderr, optionGroup.allReporters, reporterOptions.repeatErrorsMax)
}
// Make sure that the process exits with an error code.
throw MetalavaCliException(exitCode = -1)
}
}
}