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 }