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