• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2017 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
18 
19 import com.android.SdkConstants.ANDROID_URI
20 import com.android.SdkConstants.ATTR_MIN_SDK_VERSION
21 import com.android.SdkConstants.ATTR_NAME
22 import com.android.SdkConstants.TAG_PERMISSION
23 import com.android.SdkConstants.TAG_USES_SDK
24 import com.android.tools.metalava.CodebaseComparator
25 import com.android.tools.metalava.ComparisonVisitor
26 import com.android.tools.metalava.Issues
27 import com.android.tools.metalava.model.psi.CodePrinter
28 import com.android.tools.metalava.model.text.TextBackedAnnotationItem
29 import com.android.tools.metalava.model.visitors.ItemVisitor
30 import com.android.tools.metalava.model.visitors.TypeVisitor
31 import com.android.tools.metalava.reporter
32 import com.android.utils.XmlUtils.getFirstSubTagByName
33 import com.android.utils.XmlUtils.getNextTagByName
34 import com.intellij.psi.PsiFile
35 import org.objectweb.asm.Type
36 import org.objectweb.asm.tree.ClassNode
37 import org.objectweb.asm.tree.FieldInsnNode
38 import org.objectweb.asm.tree.FieldNode
39 import org.objectweb.asm.tree.MethodInsnNode
40 import org.objectweb.asm.tree.MethodNode
41 import java.io.File
42 import java.util.function.Predicate
43 import kotlin.text.Charsets.UTF_8
44 
45 /**
46  * Represents a complete unit of code -- typically in the form of a set
47  * of source trees, but also potentially backed by .jar files or even
48  * signature files
49  */
50 interface Codebase {
51     /** Description of what this codebase is (useful during debugging) */
52     var description: String
53 
54     /**
55      * The location of the API. Could point to a signature file, or a directory
56      * root for source files, or a jar file, etc.
57      */
58     var location: File
59 
60     /** The packages in the codebase (may include packages that are not included in the API) */
61     fun getPackages(): PackageList
62 
63     /**
64      * The package documentation, if any - this returns overview.html files for each package
65      * that provided one. Not all codebases provide this.
66      */
67     fun getPackageDocs(): PackageDocs?
68 
69     /** The rough size of the codebase (package count) */
70     fun size(): Int
71 
72     /** Returns a class identified by fully qualified name, if in the codebase */
73     fun findClass(className: String): ClassItem?
74 
75     /** Returns a package identified by fully qualified name, if in the codebase */
76     fun findPackage(pkgName: String): PackageItem?
77 
78     /** Returns true if this codebase supports documentation. */
79     fun supportsDocumentation(): Boolean
80 
81     /**
82      * Returns true if this codebase corresponds to an already trusted API (e.g.
83      * is read in from something like an existing signature file); in that case,
84      * signature checks etc will not be performed.
85      */
86     fun trustedApi(): Boolean
87 
88     fun accept(visitor: ItemVisitor) {
89         getPackages().accept(visitor)
90     }
91 
92     fun acceptTypes(visitor: TypeVisitor) {
93         getPackages().acceptTypes(visitor)
94     }
95 
96     /**
97      * Visits this codebase and compares it with another codebase, informing the visitors about
98      * the correlations and differences that it finds
99      */
100     fun compareWith(visitor: ComparisonVisitor, other: Codebase, filter: Predicate<Item>? = null) {
101         CodebaseComparator().compare(visitor, other, this, filter)
102     }
103 
104     /**
105      * Creates an annotation item for the given (fully qualified) Java source
106      */
107     fun createAnnotation(
108         source: String,
109         context: Item? = null,
110         mapName: Boolean = true
111     ): AnnotationItem = TextBackedAnnotationItem(
112         this, source, mapName
113     )
114 
115     /**
116      * Returns true if the codebase contains one or more Kotlin files
117      */
118     fun hasKotlin(): Boolean {
119         return units.any { it.fileType.name == "Kotlin" }
120     }
121 
122     /**
123      * Returns true if the codebase contains one or more Java files
124      */
125     fun hasJava(): Boolean {
126         return units.any { it.fileType.name == "JAVA" }
127     }
128 
129     /** The manifest to associate with this codebase, if any */
130     var manifest: File?
131 
132     /**
133      * Returns the permission level of the named permission, if specified
134      * in the manifest. This method should only be called if the codebase has
135      * been configured with a manifest
136      */
137     fun getPermissionLevel(name: String): String?
138 
139     fun getMinSdkVersion(): MinSdkVersion
140 
141     /** Clear the [Item.tag] fields (prior to iteration like DFS) */
142     fun clearTags() {
143         getPackages().packages.forEach { pkg -> pkg.allClasses().forEach { cls -> cls.tag = false } }
144     }
145 
146     /** Reports that the given operation is unsupported for this codebase type */
147     fun unsupported(desc: String? = null): Nothing
148 
149     /** Discards this model */
150     fun dispose() {
151         description += " [disposed]"
152     }
153 
154     /** If this codebase was filtered from another codebase, this points to the original */
155     var original: Codebase?
156 
157     /** Returns the compilation units used in this codebase (may be empty
158      * when the codebase is not loaded from source, such as from .jar files or
159      * from signature files) */
160     var units: List<PsiFile>
161 
162     /**
163      * Printer which can convert PSI, UAST and constants into source code,
164      * with ability to filter out elements that are not part of a codebase etc
165      */
166     val printer: CodePrinter
167 
168     /** If true, this codebase has already been filtered */
169     val preFiltered: Boolean
170 
171     /** Finds the given class by JVM owner */
172     fun findClassByOwner(owner: String, apiFilter: Predicate<Item>): ClassItem? {
173         val className = owner.replace('/', '.').replace('$', '.')
174         val cls = findClass(className)
175         return if (cls != null && apiFilter.test(cls)) {
176             cls
177         } else {
178             null
179         }
180     }
181 
182     fun findClass(node: ClassNode, apiFilter: Predicate<Item>): ClassItem? {
183         return findClassByOwner(node.name, apiFilter)
184     }
185 
186     fun findMethod(node: MethodInsnNode, apiFilter: Predicate<Item>): MethodItem? {
187         val cls = findClassByOwner(node.owner, apiFilter) ?: return null
188         val types = Type.getArgumentTypes(node.desc)
189         val parameters = if (types.isNotEmpty()) {
190             val sb = StringBuilder()
191             for (type in types) {
192                 if (sb.isNotEmpty()) {
193                     sb.append(", ")
194                 }
195                 sb.append(type.className.replace('/', '.').replace('$', '.'))
196             }
197             sb.toString()
198         } else {
199             ""
200         }
201         val methodName = if (node.name == "<init>") cls.simpleName() else node.name
202         val method = cls.findMethod(methodName, parameters)
203         return if (method != null && apiFilter.test(method)) {
204             method
205         } else {
206             null
207         }
208     }
209 
210     fun findMethod(classNode: ClassNode, node: MethodNode, apiFilter: Predicate<Item>): MethodItem? {
211         val cls = findClass(classNode, apiFilter) ?: return null
212         val types = Type.getArgumentTypes(node.desc)
213         val parameters = if (types.isNotEmpty()) {
214             val sb = StringBuilder()
215             for (type in types) {
216                 if (sb.isNotEmpty()) {
217                     sb.append(", ")
218                 }
219                 sb.append(type.className.replace('/', '.').replace('$', '.'))
220             }
221             sb.toString()
222         } else {
223             ""
224         }
225         val methodName = if (node.name == "<init>") cls.simpleName() else node.name
226         val method = cls.findMethod(methodName, parameters)
227         return if (method != null && apiFilter.test(method)) {
228             method
229         } else {
230             null
231         }
232     }
233 
234     fun findField(classNode: ClassNode, node: FieldNode, apiFilter: Predicate<Item>): FieldItem? {
235         val cls = findClass(classNode, apiFilter) ?: return null
236         val field = cls.findField(node.name)
237         return if (field != null && apiFilter.test(field)) {
238             field
239         } else {
240             null
241         }
242     }
243 
244     fun findField(node: FieldInsnNode, apiFilter: Predicate<Item>): FieldItem? {
245         val cls = findClassByOwner(node.owner, apiFilter) ?: return null
246         val field = cls.findField(node.name)
247         return if (field != null && apiFilter.test(field)) {
248             field
249         } else {
250             null
251         }
252     }
253 
254     fun isEmpty(): Boolean {
255         return getPackages().packages.isEmpty()
256     }
257 }
258 
259 sealed class MinSdkVersion
260 data class SetMinSdkVersion(val value: Int) : MinSdkVersion()
261 object UnsetMinSdkVersion : MinSdkVersion()
262 
263 abstract class DefaultCodebase(override var location: File) : Codebase {
264     override var manifest: File? = null
265     private var permissions: Map<String, String>? = null
266     private var minSdkVersion: MinSdkVersion? = null
267     override var original: Codebase? = null
268     override var units: List<PsiFile> = emptyList()
269     @Suppress("LeakingThis")
270     override val printer = CodePrinter(this)
271     @Suppress("LeakingThis")
272     override var preFiltered: Boolean = original != null
273 
getPermissionLevelnull274     override fun getPermissionLevel(name: String): String? {
275         if (permissions == null) {
276             assert(manifest != null) {
277                 "This method should only be called when a manifest has been configured on the codebase"
278             }
279             try {
280                 val map = HashMap<String, String>(600)
281                 val doc = parseDocument(manifest?.readText(UTF_8) ?: "", true)
282                 var current = getFirstSubTagByName(doc.documentElement, TAG_PERMISSION)
283                 while (current != null) {
284                     val permissionName = current.getAttributeNS(ANDROID_URI, ATTR_NAME)
285                     val protectionLevel = current.getAttributeNS(ANDROID_URI, "protectionLevel")
286                     map[permissionName] = protectionLevel
287                     current = getNextTagByName(current, TAG_PERMISSION)
288                 }
289                 permissions = map
290             } catch (error: Throwable) {
291                 reporter.report(Issues.PARSE_ERROR, manifest, "Failed to parse $manifest: ${error.message}")
292                 permissions = emptyMap()
293             }
294         }
295 
296         return permissions!![name]
297     }
298 
getMinSdkVersionnull299     override fun getMinSdkVersion(): MinSdkVersion {
300         if (minSdkVersion == null) {
301             if (manifest == null) {
302                 minSdkVersion = UnsetMinSdkVersion
303                 return minSdkVersion!!
304             }
305             minSdkVersion = try {
306                 val doc = parseDocument(manifest?.readText(UTF_8) ?: "", true)
307                 val usesSdk = getFirstSubTagByName(doc.documentElement, TAG_USES_SDK)
308                 if (usesSdk == null) {
309                     UnsetMinSdkVersion
310                 } else {
311                     val value = usesSdk.getAttributeNS(ANDROID_URI, ATTR_MIN_SDK_VERSION)
312                     if (value.isEmpty()) UnsetMinSdkVersion else SetMinSdkVersion(value.toInt())
313                 }
314             } catch (error: Throwable) {
315                 reporter.report(Issues.PARSE_ERROR, manifest, "Failed to parse $manifest: ${error.message}")
316                 UnsetMinSdkVersion
317             }
318         }
319         return minSdkVersion!!
320     }
321 
getPackageDocsnull322     override fun getPackageDocs(): PackageDocs? = null
323 
324     override fun unsupported(desc: String?): Nothing {
325         error(desc ?: "This operation is not available on this type of codebase (${this.javaClass.simpleName})")
326     }
327 }
328