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