| /* |
| * 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.apilevels.ApiGenerator |
| import com.android.tools.metalava.cli.common.ActionContext |
| 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.VersionCommand |
| import com.android.tools.metalava.cli.common.commonOptions |
| 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.UpdateSignatureHeaderCommand |
| import com.android.tools.metalava.compatibility.CompatibilityCheck |
| import com.android.tools.metalava.compatibility.CompatibilityCheck.CheckRequest |
| import com.android.tools.metalava.doc.DocAnalyzer |
| import com.android.tools.metalava.lint.ApiLint |
| 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.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.stub.StubWriter |
| import com.github.ajalt.clikt.core.subcommands |
| import com.google.common.base.Stopwatch |
| import java.io.File |
| import java.io.IOException |
| import java.io.PrintWriter |
| import java.io.StringWriter |
| import java.util.Arrays |
| import java.util.concurrent.TimeUnit.SECONDS |
| import java.util.function.Predicate |
| import kotlin.system.exitProcess |
| |
| const val PROGRAM_NAME = "metalava" |
| |
| fun main(args: Array<String>) { |
| val executionEnvironment = ExecutionEnvironment() |
| val exitCode = run(executionEnvironment = executionEnvironment, originalArgs = args) |
| |
| executionEnvironment.stdout.flush() |
| executionEnvironment.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( |
| executionEnvironment: ExecutionEnvironment, |
| originalArgs: Array<String>, |
| ): Int { |
| val stdout = executionEnvironment.stdout |
| val stderr = executionEnvironment.stderr |
| |
| // 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( |
| executionEnvironment, |
| progressTracker, |
| ) |
| val exitCode = command.process(modifiedArgs) |
| |
| stdout.flush() |
| stderr.flush() |
| |
| progressTracker.progress("$PROGRAM_NAME exiting with exit code $exitCode\n") |
| |
| return exitCode |
| } |
| |
| @Suppress("DEPRECATION") |
| internal fun processFlags( |
| environmentManager: EnvironmentManager, |
| progressTracker: ProgressTracker |
| ) { |
| val stopwatch = Stopwatch.createStarted() |
| |
| val reporter = options.reporter |
| val annotationManager = options.annotationManager |
| val sourceParser = |
| environmentManager.createSourceParser( |
| reporter = reporter, |
| annotationManager = annotationManager, |
| javaLanguageLevel = options.javaLanguageLevelAsString, |
| kotlinLanguageLevel = options.kotlinLanguageLevelAsString, |
| useK2Uast = options.useK2Uast, |
| jdkHome = options.jdkHome, |
| ) |
| |
| val signatureFileCache = SignatureFileCache(annotationManager) |
| |
| val actionContext = |
| ActionContext( |
| progressTracker = progressTracker, |
| reporter = reporter, |
| sourceParser = sourceParser, |
| ) |
| |
| val classResolverProvider = |
| ClassResolverProvider( |
| sourceParser = sourceParser, |
| apiClassResolution = options.apiClassResolution, |
| classpath = options.classpath, |
| ) |
| |
| 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 textCodebase = |
| SignatureFileLoader.loadFiles( |
| sources, |
| classResolverProvider.classResolver, |
| annotationManager, |
| ) |
| |
| // 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) { |
| actionContext.loadFromJarFile(options.apiJar!!) |
| } else if (sources.size == 1 && sources[0].path.endsWith(DOT_JAR)) { |
| actionContext.loadFromJarFile(sources[0]) |
| } else if (sources.isNotEmpty() || options.sourcePath.isNotEmpty()) { |
| actionContext.loadFromSources(signatureFileCache) |
| } else { |
| return |
| } |
| |
| progressTracker.progress( |
| "$PROGRAM_NAME analyzed API in ${stopwatch.elapsed(SECONDS)} seconds\n" |
| ) |
| |
| options.subtractApi?.let { |
| progressTracker.progress("Subtracting API: ") |
| actionContext.subtractApi(signatureFileCache, codebase, it) |
| } |
| |
| if (options.hideAnnotations.matchesAnnotationName(ANDROID_FLAGGED_API)) { |
| reallyHideFlaggedSystemApis(codebase) |
| } |
| |
| val androidApiLevelXml = options.generateApiLevelXml |
| val apiLevelJars = options.apiLevelJars |
| val apiGenerator = ApiGenerator(signatureFileCache) |
| if (androidApiLevelXml != null && apiLevelJars != null) { |
| assert(options.currentApiLevel != -1) |
| |
| progressTracker.progress( |
| "Generating API levels XML descriptor file, ${androidApiLevelXml.name}: " |
| ) |
| val sdkJarRoot = options.sdkJarRoot |
| val sdkInfoFile = options.sdkInfoFile |
| val sdkExtArgs: ApiGenerator.SdkExtensionsArguments? = |
| if (sdkJarRoot != null && sdkInfoFile != null) { |
| ApiGenerator.SdkExtensionsArguments( |
| sdkJarRoot, |
| sdkInfoFile, |
| options.latestReleasedSdkExtension |
| ) |
| } else { |
| null |
| } |
| apiGenerator.generateXml( |
| apiLevelJars, |
| options.firstApiLevel, |
| options.currentApiLevel, |
| options.isDeveloperPreviewBuild(), |
| androidApiLevelXml, |
| codebase, |
| sdkExtArgs, |
| 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, options.reporterApiLint) |
| 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) { |
| actionContext.checkCompatibility(signatureFileCache, classResolverProvider, codebase, check) |
| } |
| |
| val previousApiFile = options.migrateNullsFrom |
| if (previousApiFile != null) { |
| val previous = |
| if (previousApiFile.path.endsWith(DOT_JAR)) { |
| actionContext.loadFromJarFile(previousApiFile) |
| } else { |
| signatureFileCache.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) |
| } |
| } |
| } |
| } |
| |
| private fun ActionContext.subtractApi( |
| signatureFileCache: SignatureFileCache, |
| codebase: Codebase, |
| subtractApiFile: File, |
| ) { |
| val path = subtractApiFile.path |
| val oldCodebase = |
| when { |
| path.endsWith(DOT_TXT) -> signatureFileCache.load(subtractApiFile) |
| path.endsWith(DOT_JAR) -> loadFromJarFile(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") |
| private fun ActionContext.checkCompatibility( |
| signatureFileCache: SignatureFileCache, |
| classResolverProvider: ClassResolverProvider, |
| newCodebase: Codebase, |
| check: CheckRequest, |
| ) { |
| progressTracker.progress("Checking API compatibility ($check): ") |
| val signatureFile = check.file |
| |
| val oldCodebase = |
| if (signatureFile.path.endsWith(DOT_JAR)) { |
| loadFromJarFile(signatureFile) |
| } else { |
| signatureFileCache.load(signatureFile, classResolverProvider.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! |
| // Some things to watch out for: |
| // * There is no guarantee that the signature file is actually a txt file, it could also be |
| // a `jar` file, so double check that first. |
| // * Reading two files that may be a couple of MBs each isn't a particularly fast path so |
| // check the lengths first and then compare contents byte for byte so that it exits |
| // quickly if they're different and does not do all the UTF-8 conversions. |
| options.apiFile?.let { apiFile -> |
| val compatibilityCheckCanBeSkipped = |
| signatureFile.extension == "txt" && compareFileContents(apiFile, signatureFile) |
| // TODO(b/301282006): Remove global variable use when this can be tested properly |
| fastPathCheckResult = compatibilityCheckCanBeSkipped |
| if (compatibilityCheckCanBeSkipped) return |
| } |
| |
| val baseApiFile = options.baseApiForCompatCheck |
| if (baseApiFile != null) { |
| baseApi = signatureFileCache.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, |
| ) |
| } |
| |
| /** Compare two files to see if they are byte for byte identical. */ |
| private fun compareFileContents(file1: File, file2: File): Boolean { |
| // First check the lengths, if they are different they cannot be identical. |
| if (file1.length() == file2.length()) { |
| // Then load the contents in chunks to see if they differ. |
| file1.inputStream().buffered().use { stream1 -> |
| file2.inputStream().buffered().use { stream2 -> |
| val buffer1 = ByteArray(DEFAULT_BUFFER_SIZE) |
| val buffer2 = ByteArray(DEFAULT_BUFFER_SIZE) |
| do { |
| val c1 = stream1.read(buffer1) |
| val c2 = stream2.read(buffer2) |
| if (c1 != c2) { |
| // This should never happen as the files are the same length. |
| break |
| } |
| if (c1 == -1) { |
| // They have both reached the end of file. |
| return true |
| } |
| // Check the buffer contents, if they differ exit the loop otherwise, continue |
| // on to read the next chunks. |
| } while (Arrays.equals(buffer1, 0, c1, buffer2, 0, c2)) |
| } |
| } |
| } |
| return false |
| } |
| |
| /** |
| * Used to store whether the fast path check in the previous method succeeded or not that can be |
| * checked by tests. |
| * |
| * The test must initialize it to `null`. Then if the fast path check is run it will set it a |
| * non-null to indicate whether the fast path was taken or not. The test can then differentiate |
| * between the following states: |
| * * `null` - the fast path check was not performed. |
| * * `false` - the fast path check was performed and the fast path was not taken. |
| * * `true` - the fast path check was performed and the fast path was taken. |
| * |
| * This is used because there is no nice way to test this code in isolation but the code needs to be |
| * updated to deal with some test failures. This is a hack to avoid a catch-22 where this code needs |
| * to be refactored to allow it to be tested but it needs to be tested before it can be safely |
| * refactored. |
| * |
| * TODO(b/301282006): Remove this variable when the fast path this can be tested properly |
| */ |
| internal var fastPathCheckResult: Boolean? = null |
| |
| 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 ActionContext.loadFromSources( |
| signatureFileCache: SignatureFileCache, |
| ): 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(previousApiFile) |
| else -> signatureFileCache.load(file = previousApiFile) |
| } |
| val apiLintReporter = options.reporterApiLint as DefaultReporter |
| ApiLint(codebase, previous, apiLintReporter, options.manifest, options.apiVisitorConfig) |
| .check() |
| 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 |
| } |
| |
| /** |
| * Avoids creating a [ClassResolver] unnecessarily as it is expensive to create but once created |
| * allows it to be reused for the same reason. |
| */ |
| private class ClassResolverProvider( |
| private val sourceParser: SourceParser, |
| private val apiClassResolution: ApiClassResolution, |
| private val classpath: List<File> |
| ) { |
| val classResolver: ClassResolver? by lazy { |
| if (apiClassResolution == ApiClassResolution.API_CLASSPATH && classpath.isNotEmpty()) { |
| sourceParser.getClassResolver(classpath) |
| } else { |
| null |
| } |
| } |
| } |
| |
| @Suppress("DEPRECATION") |
| fun ActionContext.loadFromJarFile( |
| 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( |
| executionEnvironment: ExecutionEnvironment, |
| progressTracker: ProgressTracker |
| ): MetalavaCommand { |
| val command = |
| MetalavaCommand( |
| executionEnvironment = executionEnvironment, |
| progressTracker = progressTracker, |
| defaultCommandName = "main", |
| ) |
| command.subcommands( |
| MainCommand(command.commonOptions, executionEnvironment), |
| AndroidJarsToSignaturesCommand(), |
| HelpCommand(), |
| MakeAnnotationsPackagePrivateCommand(), |
| MergeSignaturesCommand(), |
| SignatureToJDiffCommand(), |
| UpdateSignatureHeaderCommand(), |
| VersionCommand(), |
| ) |
| return command |
| } |