1 /* <lambda>null2 * Copyright (C) 2025 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.model.text 18 19 import com.android.tools.metalava.model.AnnotationItem 20 import com.android.tools.metalava.model.BaseItemVisitor 21 import com.android.tools.metalava.model.CallableItem 22 import com.android.tools.metalava.model.ClassItem 23 import com.android.tools.metalava.model.Codebase 24 import com.android.tools.metalava.model.CodebaseFragment 25 import com.android.tools.metalava.model.FieldItem 26 import com.android.tools.metalava.model.Item 27 import com.android.tools.metalava.model.MemberItem 28 import com.android.tools.metalava.model.ModifierList 29 import com.android.tools.metalava.model.PackageItem 30 import com.android.tools.metalava.model.PropertyItem 31 import com.android.tools.metalava.model.SelectableItem 32 import com.android.tools.metalava.model.snapshot.EmittableDelegatingVisitor 33 import com.android.tools.metalava.model.snapshot.NonFilteringDelegatingVisitor 34 35 /** 36 * Creates a snapshot that is a delta between two [Codebase]s. 37 * 38 * This effectively does the opposite of what [ApiFile] does when creating a [Codebase] from 39 * multiple signature files, where the first is a standalone surface and each subsequent file is for 40 * a surface that extends the surface in the preceding file. 41 * 42 * This can be used to create deltas that can be used when class nesting is not maintained as it 43 * does not emit a class just because a nested class needs emitting. 44 */ 45 class SnapshotDeltaMaker private constructor(private val base: Codebase) : 46 BaseItemVisitor( 47 preserveClassNesting = true, 48 visitParameterItems = false, 49 ) { 50 /** 51 * Mark the package to emit. 52 * 53 * The containing package is not marked to emit as packages are flattened before visiting. 54 */ 55 private fun PackageItem.markEmit() { 56 if (!emit) { 57 emit = true 58 } 59 } 60 61 /** 62 * Mark the class to emit. 63 * 64 * The containing package is marked to emit as otherwise its contents will not usually be 65 * visited. The containing class of nested classes is not marked to emit as this is used for 66 * files that flatten nested classes so nested classes can be visited without checking the 67 * [SelectableItem.emit] of the containing class. 68 */ 69 private fun ClassItem.markEmit() { 70 if (!emit) { 71 emit = true 72 containingPackage().markEmit() 73 } 74 } 75 76 /** 77 * Mark a member to emit. 78 * 79 * The containing class is marked to emit as otherwise its members will not be visited. 80 */ 81 private fun MemberItem.markEmit() { 82 if (!emit) { 83 emit = true 84 containingClass().markEmit() 85 } 86 } 87 88 /** Override to visit all packages. */ 89 override fun skipPackage(pkg: PackageItem) = false 90 91 /** Override to skip any non-public or protected items. */ 92 override fun skip(item: Item): Boolean = !item.modifiers.isPublicOrProtected() 93 94 /** Convert a list of [AnnotationItem]s into a list of [String]s for comparison. */ 95 // TODO(b/354633349): Use equality once value abstraction provides consistent behavior across 96 // models. 97 private fun List<AnnotationItem>.normalize() = map { it.toString() }.sorted() 98 99 override fun visitClass(cls: ClassItem) { 100 cls.findCorrespondingItemIn(base)?.let { baseClass -> 101 // If super class type is set and is different to the base class then drop out to emit 102 // this class. 103 val superClassType = cls.superClassType() 104 if (superClassType != null && baseClass.superClassType() != superClassType) { 105 return@let 106 } 107 108 // If this class has different annotations to the base class then drop out to emit 109 // this class. 110 val annotations = cls.modifiers.annotations().normalize() 111 val baseAnnotations = baseClass.modifiers.annotations().normalize() 112 if (annotations != baseAnnotations) { 113 return@let 114 } 115 116 // The class is not different so do not emit it. 117 return 118 } 119 120 // The class is new or different. 121 cls.markEmit() 122 } 123 124 override fun visitCallable(callable: CallableItem) { 125 callable.findCorrespondingItemIn(base)?.let { 126 return 127 } 128 129 // The callable is new. 130 callable.markEmit() 131 } 132 133 override fun visitField(field: FieldItem) { 134 field.findCorrespondingItemIn(base)?.let { 135 return 136 } 137 138 // The field is new. 139 field.markEmit() 140 } 141 142 override fun visitProperty(property: PropertyItem) { 143 property.findCorrespondingItemIn(base)?.let { 144 return 145 } 146 147 // The property is new. 148 property.markEmit() 149 } 150 151 companion object { 152 /** 153 * Create a text [Codebase] that is a delta between [base] and [codebaseFragment], i.e. it 154 * includes all the [Item] that are in [codebaseFragment] but not in [base]. 155 * 156 * This is expected to be used where [codebaseFragment] is a super set of [base] but that is 157 * not enforced. If [base] contains [Item]s which are not present in [codebaseFragment] then 158 * they will not appear in the delta. 159 * 160 * [ClassItem]s are treated specially. If [codebaseFragment] and [base] have [ClassItem]s 161 * with the same name and [codebaseFragment]'s has members which are not present in [base]'s 162 * then a [ClassItem] containing the additional [codebaseFragment] members will appear in 163 * the delta, otherwise it will not unless the two [ClassItem]s differ in one of the 164 * following ways: 165 * * The modifiers are not [ModifierList.equivalentTo] each other. 166 * * The [ClassItem.superClassType]s are not the same. 167 * 168 * Note: A [MemberItem] that exists in both will not be emitted even if they differ in some 169 * way, e.g. annotations, extends list. That is because [ApiFile] has no mechanism to 170 * combine them and does not even throw an error if it encounters duplicates. 171 */ 172 fun createDelta( 173 base: Codebase, 174 codebaseFragment: CodebaseFragment, 175 ): CodebaseFragment { 176 // Take a snapshot. 177 val snapshotFragment = 178 codebaseFragment.snapshotIncludingRevertedItems( 179 referenceVisitorFactory = ::NonFilteringDelegatingVisitor, 180 ) 181 182 val snapshot = snapshotFragment.codebase 183 184 // Assume that none of it will be emitted. 185 snapshot.accept( 186 object : BaseItemVisitor() { 187 override fun visitSelectableItem(item: SelectableItem) { 188 item.emit = false 189 } 190 } 191 ) 192 193 // Mark those items that are new (or different) to be emitted. Also, marks their 194 // containers, e.g. class members and nested classes will mark their containing class, 195 // classes will mark their containing package. 196 val deltaMaker = SnapshotDeltaMaker(base) 197 snapshot.accept(deltaMaker) 198 199 return CodebaseFragment.create(snapshot, ::EmittableDelegatingVisitor) 200 } 201 } 202 } 203