Home | History | Annotate | Download | only in metalava
      1 /*
      2  * Copyright (C) 2018 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tools.metalava
     18 
     19 import com.android.tools.metalava.doclava1.ApiPredicate
     20 import com.android.tools.metalava.doclava1.Errors
     21 import com.android.tools.metalava.doclava1.FilterPredicate
     22 import com.android.tools.metalava.model.Codebase
     23 import com.android.tools.metalava.model.Item
     24 import java.io.File
     25 import java.io.IOException
     26 import java.io.PrintWriter
     27 import java.io.StringWriter
     28 import java.util.function.Predicate
     29 
     30 /**
     31  * The [AnnotationsDiffer] can take a codebase with annotations, and subtract
     32  * another codebase with annotations, and emit a signature file that contains
     33  * *only* the signatures that have annotations that were not present in the
     34  * second codebase.
     35  *
     36  * The usecase for this a scenario like the following:
     37  * - A lot of new annotations are added to the master branch
     38  * - These annotations also apply to older APIs/branches, but for practical
     39  *   reasons we can't cherrypick the CLs that added these annotations to the
     40  *   older branches -- sometimes because those branches are under more strict
     41  *   access control, sometimes because the changes contain non-annotation
     42  *   changes as well, and sometimes because even a pure annotation CL won't
     43  *   cleanly apply to older branches since other content around the annotations
     44  *   have changed.
     45  * - We want to merge these annotations into the older codebase as an external
     46  *   annotation file. However, we don't really want to check in the *entire*
     47  *   signature file, since it's massive (which will slow down build times in
     48  *   that older branch), may leak new APIs etc.
     49  * - Solution: We can produce a "diff": create a signature file which contains
     50  *   *only* the signatures that have annotations from the new branch where
     51  *   (a) the signature is also present in the older codebase, and (b) where
     52  *   the annotation is not also present in the older codebase.
     53  *
     54  * That's what this codebase is used for: "take the master signature files
     55  * with annotations, subtract out the signature files from say the P release,
     56  * and check that in as the "annotations to import from master into P" delta
     57  * file.
     58  */
     59 class AnnotationsDiffer(
     60     superset: Codebase,
     61     private val codebase: Codebase
     62 ) {
     63     private val relevant = HashSet<Item>(1000)
     64 
     65     private val predicate = object : Predicate<Item> {
     66         override fun test(item: Item): Boolean {
     67             if (relevant.contains(item)) {
     68                 return true
     69             }
     70 
     71             val parent = item.parent() ?: return false
     72             return test(parent)
     73         }
     74     }
     75 
     76     init {
     77         // Limit the API to the codebase, and look at the super set codebase
     78         // for annotations that are only there (not in the current codebase)
     79         // and emit those
     80         val visitor = object : ComparisonVisitor() {
     81             override fun compare(old: Item, new: Item) {
     82                 val newModifiers = new.modifiers
     83                 for (annotation in old.modifiers.annotations()) {
     84                     var addAnnotation = false
     85                     if (annotation.isNullnessAnnotation()) {
     86                         if (!newModifiers.hasNullnessInfo()) {
     87                             addAnnotation = true
     88                         }
     89                     } else {
     90                         // TODO: Check for other incompatibilities than nullness?
     91                         val qualifiedName = annotation.qualifiedName() ?: continue
     92                         if (newModifiers.findAnnotation(qualifiedName) == null) {
     93                             addAnnotation = true
     94                         }
     95                     }
     96 
     97                     if (addAnnotation) {
     98                         relevant.add(new)
     99                     }
    100                 }
    101             }
    102         }
    103         CodebaseComparator().compare(visitor, superset, codebase, ApiPredicate(codebase))
    104     }
    105 
    106     fun writeDiffSignature(apiFile: File) {
    107         val apiFilter = FilterPredicate(ApiPredicate(codebase))
    108         val apiReference = ApiPredicate(codebase, ignoreShown = true)
    109         val apiEmit = apiFilter.and(predicate)
    110 
    111         progress("\nWriting annotation diff file: ")
    112         try {
    113             val stringWriter = StringWriter()
    114             val writer = PrintWriter(stringWriter)
    115             writer.use { printWriter ->
    116                 val preFiltered = codebase.original != null
    117                 val apiWriter = SignatureWriter(printWriter, apiEmit, apiReference, preFiltered)
    118                 codebase.accept(apiWriter)
    119             }
    120 
    121             // Clean up blank lines
    122             var prev: Char = ' '
    123             val cleanedUp = stringWriter.toString().filter {
    124                 if (it == '\n' && prev == '\n')
    125                     false
    126                 else {
    127                     prev = it
    128                     true
    129                 }
    130             }
    131 
    132             apiFile.writeText(cleanedUp, Charsets.UTF_8)
    133         } catch (e: IOException) {
    134             reporter.report(Errors.IO_ERROR, apiFile, "Cannot open file for write.")
    135         }
    136     }
    137 }