blob: f5f4bf72d8da0c107754b9915bb53a0afca1291a [file] [log] [blame]
/*
* Copyright (C) 2018 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.
*/
package com.android.tools.metalava
import com.android.tools.metalava.doclava1.ApiPredicate
import com.android.tools.metalava.doclava1.Issues
import com.android.tools.metalava.doclava1.FilterPredicate
import com.android.tools.metalava.model.Codebase
import com.android.tools.metalava.model.Item
import java.io.File
import java.io.IOException
import java.io.PrintWriter
import java.io.StringWriter
import java.util.function.Predicate
import kotlin.text.Charsets.UTF_8
/**
* The [AnnotationsDiffer] can take a codebase with annotations, and subtract
* another codebase with annotations, and emit a signature file that contains
* *only* the signatures that have annotations that were not present in the
* second codebase.
*
* The usecase for this a scenario like the following:
* - A lot of new annotations are added to the master branch
* - These annotations also apply to older APIs/branches, but for practical
* reasons we can't cherrypick the CLs that added these annotations to the
* older branches -- sometimes because those branches are under more strict
* access control, sometimes because the changes contain non-annotation
* changes as well, and sometimes because even a pure annotation CL won't
* cleanly apply to older branches since other content around the annotations
* have changed.
* - We want to merge these annotations into the older codebase as an external
* annotation file. However, we don't really want to check in the *entire*
* signature file, since it's massive (which will slow down build times in
* that older branch), may leak new APIs etc.
* - Solution: We can produce a "diff": create a signature file which contains
* *only* the signatures that have annotations from the new branch where
* (a) the signature is also present in the older codebase, and (b) where
* the annotation is not also present in the older codebase.
*
* That's what this codebase is used for: "take the master signature files
* with annotations, subtract out the signature files from say the P release,
* and check that in as the "annotations to import from master into P" delta
* file.
*/
class AnnotationsDiffer(
private val superset: Codebase,
codebase: Codebase
) {
private val relevant = HashSet<Item>(1000)
private val predicate = object : Predicate<Item> {
override fun test(item: Item): Boolean {
if (relevant.contains(item)) {
return true
}
val parent = item.parent() ?: return false
return test(parent)
}
}
init {
// Limit the API to the codebase, and look at the super set codebase
// for annotations that are only there (not in the current codebase)
// and emit those
val visitor = object : ComparisonVisitor() {
override fun compare(old: Item, new: Item) {
val newModifiers = new.modifiers
for (annotation in old.modifiers.annotations()) {
var addAnnotation = false
if (annotation.isNullnessAnnotation()) {
if (!newModifiers.hasNullnessInfo()) {
addAnnotation = true
}
} else {
// TODO: Check for other incompatibilities than nullness?
val qualifiedName = annotation.qualifiedName() ?: continue
if (newModifiers.findAnnotation(qualifiedName) == null) {
addAnnotation = true
}
}
if (addAnnotation) {
relevant.add(new)
}
}
}
}
val filter =
if (codebase.supportsDocumentation()) {
ApiPredicate()
} else {
Predicate<Item> { true }
}
CodebaseComparator().compare(visitor, superset, codebase, filter)
}
fun writeDiffSignature(apiFile: File) {
val codebase = superset
val apiFilter = FilterPredicate(ApiPredicate())
val apiReference = ApiPredicate(ignoreShown = true)
val apiEmit = apiFilter.and(predicate)
progress("\nWriting annotation diff file: ")
try {
val stringWriter = StringWriter()
val writer = PrintWriter(stringWriter)
writer.use { printWriter ->
val apiWriter = SignatureWriter(printWriter, apiEmit, apiReference, codebase.preFiltered)
codebase.accept(apiWriter)
}
// Clean up blank lines
var prev = ' '
val cleanedUp = stringWriter.toString().filter {
if (it == '\n' && prev == '\n')
false
else {
prev = it
true
}
}
apiFile.writeText(cleanedUp, UTF_8)
} catch (e: IOException) {
reporter.report(Issues.IO_ERROR, apiFile, "Cannot open file for write.")
}
}
}