1 /*
2  * Copyright (C) 2024 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.ClassItem
20 import com.android.tools.metalava.model.ClassKind
21 import com.android.tools.metalava.model.ClassOrigin
22 import com.android.tools.metalava.model.ClassResolver
23 import com.android.tools.metalava.model.Codebase
24 import com.android.tools.metalava.model.TypeParameterList
25 import com.android.tools.metalava.model.VisibilityLevel
26 import com.android.tools.metalava.model.createImmutableModifiers
27 import com.android.tools.metalava.model.item.DefaultClassItem
28 import com.android.tools.metalava.model.provider.Capability
29 import com.android.tools.metalava.model.provider.InputFormat
30 import com.android.tools.metalava.model.testing.transformer.CodebaseTransformer
31 import com.android.tools.metalava.model.testsuite.ModelSuiteRunner
32 import com.android.tools.metalava.reporter.FileLocation
33 import com.android.tools.metalava.testing.getAndroidJar
34 import java.io.File
35 import java.net.URLClassLoader
36 
37 // @AutoService(ModelSuiteRunner::class)
38 class TextModelSuiteRunner : ModelSuiteRunner {
39 
40     override val providerName = "text"
41 
42     override val supportedInputFormats = setOf(InputFormat.SIGNATURE)
43 
44     override val capabilities: Set<Capability> = setOf()
45 
createCodebaseAndRunnull46     override fun createCodebaseAndRun(
47         inputs: ModelSuiteRunner.TestInputs,
48         test: (Codebase) -> Unit
49     ) {
50         if (inputs.commonSourceDir != null) {
51             error("text model does not support common sources")
52         }
53 
54         val testFixture = inputs.testFixture
55         val codebaseConfig = testFixture.codebaseConfig
56 
57         val signatureFiles = SignatureFile.forTest(inputs.mainSourceDir.createFiles())
58         val resolver = ClassLoaderBasedClassResolver(getAndroidJar(), codebaseConfig)
59         val codebase =
60             ApiFile.parseApi(
61                 signatureFiles,
62                 codebaseConfig = codebaseConfig,
63                 classResolver = resolver,
64             )
65 
66         // If available, transform the codebase for testing, otherwise use the one provided.
67         val transformedCodebase = CodebaseTransformer.transformIfAvailable(codebase)
68 
69         test(transformedCodebase)
70     }
71 
toStringnull72     override fun toString() = providerName
73 }
74 
75 /**
76  * A [ClassResolver] that is backed by a [URLClassLoader].
77  *
78  * When [resolveClass] is called this will first look in [codebase] to see if the [ClassItem] has
79  * already been loaded, returning it if found. Otherwise, it will look in the [classLoader] to see
80  * if the class exists on the classpath. If it does then it will create a [DefaultClassItem] to
81  * represent it and add it to the [codebase]. Otherwise, it will return `null`.
82  *
83  * The created [DefaultClassItem] is not a complete representation of the class that was found in
84  * the [classLoader]. It is just a placeholder to indicate that it was found, although that may
85  * change in the future.
86  */
87 class ClassLoaderBasedClassResolver(
88     jar: File,
89     codebaseConfig: Codebase.Config = Codebase.Config.NOOP,
90 ) : ClassResolver {
91 
92     private val assembler by
93         lazy(LazyThreadSafetyMode.NONE) {
94             TextCodebaseAssembler.createAssembler(
95                 location = jar,
96                 description = "Codebase for resolving classes in $jar for tests",
97                 codebaseConfig = codebaseConfig,
98                 classResolver = null,
99             )
100         }
101 
102     private val codebase by lazy(LazyThreadSafetyMode.NONE) { assembler.codebase }
103 
104     private val classLoader by
105         lazy(LazyThreadSafetyMode.NONE) { URLClassLoader(arrayOf(jar.toURI().toURL()), null) }
106 
107     private fun findClassInClassLoader(qualifiedName: String): Class<*>? {
108         var binaryName = qualifiedName
109         do {
110             try {
111                 return classLoader.loadClass(binaryName)
112             } catch (e: ClassNotFoundException) {
113                 // If the class could not be found then maybe it was a nested class so replace the
114                 // last '.' in the name with a $ and try again. If there is no '.' then return.
115                 val lastDot = binaryName.lastIndexOf('.')
116                 if (lastDot == -1) {
117                     return null
118                 } else {
119                     val before = binaryName.substring(0, lastDot)
120                     val after = binaryName.substring(lastDot + 1)
121                     binaryName = "$before\$$after"
122                 }
123             }
124         } while (true)
125     }
126 
127     override fun resolveClass(erasedName: String): ClassItem? {
128         return codebase.findClass(erasedName)
129             ?: run {
130                 val cls = findClassInClassLoader(erasedName) ?: return null
131                 val packageName = cls.`package`.name
132 
133                 val itemFactory = assembler.itemFactory
134 
135                 val packageItem = codebase.findOrCreatePackage(packageName)
136                 itemFactory.createClassItem(
137                     fileLocation = FileLocation.UNKNOWN,
138                     modifiers = createImmutableModifiers(VisibilityLevel.PACKAGE_PRIVATE),
139                     classKind = ClassKind.CLASS,
140                     containingClass = null,
141                     containingPackage = packageItem,
142                     qualifiedName = cls.canonicalName,
143                     typeParameterList = TypeParameterList.NONE,
144                     origin = ClassOrigin.CLASS_PATH,
145                     superClassType = null,
146                     interfaceTypes = emptyList(),
147                 )
148             }
149     }
150 }
151