blob: 8d1822f18538a6eb6b63c8ed1e0a7d97659656ba [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.model.AnnotationManager
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.FileFormat
import com.android.tools.metalava.model.Item
import com.android.tools.metalava.model.psi.PsiBasedClassResolver
import com.android.tools.metalava.model.psi.PsiBasedCodebase
import com.android.tools.metalava.model.psi.PsiEnvironmentManager
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.stub.StubWriter
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>) {
run(args, setExitCode = true)
}
internal var hasFileReadViolations = false
/**
* 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 = PrintWriter(OutputStreamWriter(System.out)),
stderr: PrintWriter = PrintWriter(OutputStreamWriter(System.err)),
setExitCode: Boolean = false
): Boolean {
var exitCode = 0
try {
val modifiedArgs = preprocessArgv(originalArgs)
progress("$PROGRAM_NAME started\n")
// Dump the arguments, and maybe generate a rerun-script.
maybeDumpArgv(stdout, originalArgs, modifiedArgs)
// Actual work begins here.
val command = MetalavaCommand(stdout, stderr)
command.process(modifiedArgs)
if (options.allReporters.any { it.hasErrors() } && !options.passBaselineUpdates) {
// Repeat the errors at the end to make it easy to find the actual problems.
if (options.repeatErrorsMax > 0) {
repeatErrors(stderr, options.allReporters, options.repeatErrorsMax)
}
exitCode = -1
}
if (hasFileReadViolations) {
if (options.strictInputFiles.shouldFail) {
stderr.print("Error: ")
exitCode = -1
} else {
stderr.print("Warning: ")
}
stderr.println(
"$PROGRAM_NAME detected access to files that are not explicitly specified. See ${options.strictInputViolationsFile} for details."
)
}
} catch (e: DriverException) {
stdout.flush()
stderr.flush()
val prefix =
if (e.exitCode != 0) {
"Aborting: "
} else {
""
}
if (e.stderr.isNotBlank()) {
stderr.println("\n${prefix}${e.stderr}")
}
if (e.stdout.isNotBlank()) {
stdout.println("\n${prefix}${e.stdout}")
}
exitCode = e.exitCode
}
// Update and close all baseline files.
options.allBaselines.forEach { baseline ->
if (options.verbose) {
baseline.dumpStats(options.stdout)
}
if (baseline.close()) {
if (!options.quiet) {
stdout.println("$PROGRAM_NAME wrote updated baseline to ${baseline.updateFile}")
}
}
}
options.reportEvenIfSuppressedWriter?.close()
options.strictInputViolationsPrintWriter?.close()
// Show failure messages, if any.
options.allReporters.forEach { it.writeErrorMessage(stderr) }
stdout.flush()
stderr.flush()
if (setExitCode) {
exit(exitCode)
}
return exitCode == 0
}
private fun exit(exitCode: Int = 0) {
if (options.verbose) {
progress("$PROGRAM_NAME exiting with exit code $exitCode\n")
}
options.stdout.flush()
options.stderr.flush()
exitProcess(exitCode)
}
internal fun maybeActivateSandbox() {
// Set up a sandbox to detect access to files that are not explicitly specified.
if (options.strictInputFiles == Options.StrictInputFileMode.PERMISSIVE) {
return
}
val writer = options.strictInputViolationsPrintWriter!!
// Writes all violations to [Options.strictInputFiles].
// If Options.StrictInputFile.Mode is STRICT, then all violations on reads are logged, and the
// tool exits with a negative error code if there are any file read violations. Directory read
// violations are logged, but are considered to be a "warning" and doesn't affect the exit code.
// If STRICT_WARN, all violations on reads are logged similar to STRICT, but the exit code is
// unaffected.
// If STRICT_WITH_STACK, similar to STRICT, but also logs the stack trace to
// Options.strictInputFiles.
// See [FileReadSandbox] for the details.
FileReadSandbox.activate(
object : FileReadSandbox.Listener {
var seen = mutableSetOf<String>()
override fun onViolation(absolutePath: String, isDirectory: Boolean) {
if (!seen.contains(absolutePath)) {
val suffix = if (isDirectory) "/" else ""
writer.println("$absolutePath$suffix")
if (options.strictInputFiles == Options.StrictInputFileMode.STRICT_WITH_STACK) {
Throwable().printStackTrace(writer)
}
seen.add(absolutePath)
if (!isDirectory) {
hasFileReadViolations = true
}
}
}
}
)
}
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."
)
}
}
internal fun processFlags(psiEnvironmentManager: PsiEnvironmentManager) {
val stopwatch = Stopwatch.createStarted()
processNonCodebaseFlags()
val psiSourceParser =
PsiSourceParser(
psiEnvironmentManager,
javaLanguageLevel = options.javaLanguageLevel,
kotlinLanguageLevel = options.kotlinLanguageLevel,
)
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 DriverException(
"Inconsistent input file types: The first file is of $DOT_TXT, but detected different extension in ${it.path}"
)
}
val classResolver = getClassResolver(psiSourceParser)
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(psiSourceParser, textCodebase)
}
textCodebase
} else if (options.apiJar != null) {
loadFromJarFile(psiSourceParser, options.apiJar!!)
} else if (sources.size == 1 && sources[0].path.endsWith(DOT_JAR)) {
loadFromJarFile(psiSourceParser, sources[0])
} else if (sources.isNotEmpty() || options.sourcePath.isNotEmpty()) {
loadFromSources(psiSourceParser)
} else {
return
}
if (options.verbose) {
progress("$PROGRAM_NAME analyzed API in ${stopwatch.elapsed(SECONDS)} seconds\n")
}
options.subtractApi?.let {
progress("Subtracting API: ")
subtractApi(psiSourceParser, codebase, it)
}
val androidApiLevelXml = options.generateApiLevelXml
val apiLevelJars = options.apiLevelJars
if (androidApiLevelXml != null && apiLevelJars != null) {
assert(options.currentApiLevel != -1)
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.")
}
progress("Enhancing docs: ")
val docAnalyzer = DocAnalyzer(codebase)
docAnalyzer.enhance()
val applyApiLevelsXml = options.applyApiLevelsXml
if (applyApiLevelsXml != null) {
progress("Applying API levels")
docAnalyzer.applyApiLevels(applyApiLevelsXml)
}
}
val apiVersionsJson = options.generateApiVersionsJson
val apiVersionNames = options.apiVersionNames
if (apiVersionsJson != null && apiVersionNames != null) {
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(
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()
val apiReference = apiType.getReferenceFilter()
createReportFile(codebase, apiFile, "API") { printWriter ->
SignatureWriter(
printWriter,
apiEmit,
apiReference,
codebase.preFiltered,
methodComparator = options.apiOverloadedMethodOrder.comparator
)
}
}
options.apiXmlFile?.let { apiFile ->
val apiType = ApiType.PUBLIC_API
val apiEmit = apiType.getEmitFilter()
val apiReference = apiType.getReferenceFilter()
createReportFile(codebase, apiFile, "XML API") { printWriter ->
JDiffXmlWriter(printWriter, apiEmit, apiReference, codebase.preFiltered)
}
}
options.removedApiFile?.let { apiFile ->
val unfiltered = codebase.original ?: codebase
val apiType = ApiType.REMOVED
val removedEmit = apiType.getEmitFilter()
val removedReference = apiType.getReferenceFilter()
createReportFile(
unfiltered,
apiFile,
"removed API",
options.deleteEmptyRemovedSignatures
) { printWriter ->
SignatureWriter(
printWriter,
removedEmit,
removedReference,
codebase.original != null,
options.includeSignatureFormatVersionRemoved,
options.apiOverloadedMethodOrder.comparator
)
}
}
options.dexApiFile?.let { apiFile ->
val apiFilter = FilterPredicate(ApiPredicate())
val memberIsNotCloned: Predicate<Item> = Predicate { !it.isCloned() }
val apiReference = ApiPredicate(ignoreShown = true)
val dexApiEmit = memberIsNotCloned.and(apiFilter)
createReportFile(codebase, apiFile, "DEX API") { printWriter ->
DexApiWriter(printWriter, dexApiEmit, apiReference)
}
}
options.proguard?.let { proguard ->
val apiEmit = FilterPredicate(ApiPredicate())
val apiReference = ApiPredicate(ignoreShown = true)
createReportFile(codebase, proguard, "Proguard file") { printWriter ->
ProguardWriter(printWriter, apiEmit, apiReference)
}
}
options.sdkValueDir?.let { dir ->
dir.mkdirs()
SdkFileWriter(codebase, dir).generate()
}
for (check in options.compatibilityChecks) {
checkCompatibility(psiSourceParser, codebase, check)
}
val previousApiFile = options.migrateNullsFrom
if (previousApiFile != null) {
val previous =
if (previousApiFile.path.endsWith(DOT_JAR)) {
loadFromJarFile(psiSourceParser, 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(
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(codebase, it) }
if (options.verbose) {
val packageCount = codebase.size()
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.
*/
private fun addMissingItemsRequiredForGeneratingStubs(
psiSourceParser: PsiSourceParser,
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(psiSourceParser, textCodebase, options.manifest)
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(
psiSourceParser: PsiSourceParser,
codebase: Codebase,
subtractApiFile: File,
) {
val path = subtractApiFile.path
val oldCodebase =
when {
path.endsWith(DOT_TXT) -> SignatureFileLoader.load(subtractApiFile)
path.endsWith(DOT_JAR) -> loadFromJarFile(psiSourceParser, subtractApiFile)
else ->
throw DriverException(
"Unsupported $ARG_SUBTRACT_API format, expected .txt or .jar: ${subtractApiFile.name}"
)
}
CodebaseComparator()
.compare(
object : ComparisonVisitor() {
override fun compare(old: ClassItem, new: ClassItem) {
new.emit = false
}
},
oldCodebase,
codebase,
ApiType.ALL.getReferenceFilter()
)
}
fun processNonCodebaseFlags() {
// --copy-annotations?
val privateAnnotationsSource = options.privateAnnotationsSource
val privateAnnotationsTarget = options.privateAnnotationsTarget
if (privateAnnotationsSource != null && privateAnnotationsTarget != null) {
val rewrite = RewriteAnnotations()
// Support pointing to both stub-annotations and stub-annotations/src/main/java
val src = File(privateAnnotationsSource, "src${File.separator}main${File.separator}java")
val source = if (src.isDirectory) src else privateAnnotationsSource
source.listFiles()?.forEach { file ->
rewrite.modifyAnnotationSources(null, file, File(privateAnnotationsTarget, file.name))
}
}
for (convert in options.convertToXmlFiles) {
convert.process()
}
}
/** Checks compatibility of the given codebase with the codebase described in the signature file. */
fun checkCompatibility(
psiSourceParser: PsiSourceParser,
newCodebase: Codebase,
check: CheckRequest,
) {
progress("Checking API compatibility ($check): ")
val signatureFile = check.file
val oldCodebase =
if (signatureFile.path.endsWith(DOT_JAR)) {
loadFromJarFile(psiSourceParser, signatureFile)
} else {
val classResolver = getClassResolver(psiSourceParser)
SignatureFileLoader.load(signatureFile, classResolver)
}
val oldFormat = (oldCodebase as? TextCodebase)?.format
if (oldFormat != null && oldFormat > FileFormat.V1 && options.outputFormat == FileFormat.V1) {
throw DriverException(
"Cannot perform compatibility check of signature file $signatureFile in format $oldFormat without analyzing current codebase with $ARG_FORMAT=$oldFormat"
)
}
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 DriverException(
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)
}
fun createTempFile(namePrefix: String, nameSuffix: String): File {
val tempFolder = options.tempFolder
return if (tempFolder != null) {
val preferred = File(tempFolder, namePrefix + nameSuffix)
if (!preferred.exists()) {
return preferred
}
File.createTempFile(namePrefix, nameSuffix, tempFolder)
} else {
File.createTempFile(namePrefix, nameSuffix)
}
}
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))
}
}
private fun loadFromSources(psiSourceParser: PsiSourceParser): Codebase {
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.sourcePath)
}
progress("Reading Codebase: ")
val codebase =
psiSourceParser.parseSources(
sources,
"Codebase loaded from source folders",
sourcePath = options.sourcePath,
classpath = options.classpath,
)
progress("Analyzing API: ")
val analyzer = ApiAnalyzer(psiSourceParser, codebase, options.manifest)
analyzer.mergeExternalInclusionAnnotations()
analyzer.computeApi()
val filterEmit = ApiPredicate(ignoreShown = true, ignoreRemoved = false)
val apiEmit = ApiPredicate(ignoreShown = true)
val apiReference = ApiPredicate(ignoreShown = true)
// 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.
progress("Insert missing stubs methods: ")
analyzer.generateInheritedStubs(apiEmit, apiReference)
analyzer.mergeExternalQualifierAnnotations()
options.nullabilityAnnotationsValidator?.validateAllFrom(
codebase,
options.validateNullabilityFromList
)
options.nullabilityAnnotationsValidator?.report()
analyzer.handleStripping()
// General API checks for Android APIs
AndroidApiChecks().check(codebase)
if (options.checkApi) {
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(psiSourceParser, previousApiFile)
else -> SignatureFileLoader.load(file = previousApiFile)
}
val apiLintReporter = options.reporterApiLint as DefaultReporter
ApiLint.check(codebase, previous, apiLintReporter)
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) {
progress("Insert missing constructors: ")
analyzer.addConstructors(filterEmit)
}
progress("Performing misc API checks: ")
analyzer.performChecks()
return codebase
}
private fun getClassResolver(psiSourceParser: PsiSourceParser): ClassResolver? {
val apiClassResolution = options.apiClassResolution
val classpath = options.classpath
return if (apiClassResolution == ApiClassResolution.API_CLASSPATH && classpath.isNotEmpty()) {
val uastEnvironment = psiSourceParser.loadUastFromJars(classpath)
PsiBasedClassResolver(uastEnvironment, options.annotationManager, reporter)
} else {
null
}
}
fun loadFromJarFile(
psiSourceParser: PsiSourceParser,
apiJar: File,
preFiltered: Boolean = false,
annotationManager: AnnotationManager = options.annotationManager,
): Codebase {
progress("Processing jar file: ")
val environment = psiSourceParser.loadUastFromJars(listOf(apiJar))
val codebase =
PsiBasedCodebase(apiJar, "Codebase loaded from $apiJar", annotationManager, reporter)
codebase.initialize(environment, apiJar, preFiltered)
val apiEmit = ApiPredicate(ignoreShown = true)
val apiReference = ApiPredicate(ignoreShown = true)
val analyzer = ApiAnalyzer(psiSourceParser, codebase)
analyzer.mergeExternalInclusionAnnotations()
analyzer.computeApi()
analyzer.mergeExternalQualifierAnnotations()
options.nullabilityAnnotationsValidator?.validateAllFrom(
codebase,
options.validateNullabilityFromList
)
options.nullabilityAnnotationsValidator?.report()
analyzer.generateInheritedStubs(apiEmit, apiReference)
return codebase
}
internal fun disableStderrDumping(): Boolean {
val disableStderrDumping =
!assertionsEnabled() && System.getenv(ENV_VAR_METALAVA_DUMP_ARGV) == null && !isUnderTest()
return disableStderrDumping
}
private fun extractAnnotations(codebase: Codebase, file: File) {
val localTimer = Stopwatch.createStarted()
options.externalAnnotations?.let { outputFile ->
@Suppress("UNCHECKED_CAST") ExtractAnnotations(codebase, outputFile).extractAnnotations()
if (options.verbose) {
progress(
"$PROGRAM_NAME extracted annotations into $file in ${localTimer.elapsed(SECONDS)} seconds\n"
)
}
}
}
private fun createStubFiles(
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) {
progress("Generating documentation stub files: ")
} else {
progress("Generating stub files: ")
}
val localTimer = Stopwatch.createStarted()
val stubWriter =
StubWriter(
codebase = codebase,
stubsDir = stubDir,
generateAnnotations = options.generateAnnotations,
preFiltered = codebase.preFiltered,
docStubs = docStubs
)
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 != null && overview.isNotBlank()) {
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)
}
}
progress(
"$PROGRAM_NAME wrote ${if (docStubs) "documentation" else ""} stubs directory $stubDir in ${
localTimer.elapsed(SECONDS)} seconds\n"
)
}
fun createReportFile(
codebase: Codebase,
apiFile: File,
description: String?,
deleteEmptyFiles: Boolean = false,
createVisitor: (PrintWriter) -> ApiVisitor
) {
if (description != null) {
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) {
reporter.report(Issues.IO_ERROR, apiFile, "Cannot open file for write.")
}
if (description != null && options.verbose) {
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()