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.projectDescription != null) { 51 error("text model does not support project description") 52 } 53 54 val testFixture = inputs.testFixture 55 val codebaseConfig = testFixture.codebaseConfig 56 57 val signatureFiles = SignatureFile.forTest(inputs.mainSourceDir.createFiles()) 58 val classPath = listOf(getAndroidJar()) + inputs.testFixture.additionalClassPath 59 val resolver = ClassLoaderBasedClassResolver(classPath, codebaseConfig) 60 val codebase = 61 ApiFile.parseApi( 62 signatureFiles, 63 codebaseConfig = codebaseConfig, 64 classResolver = resolver, 65 ) 66 67 // If available, transform the codebase for testing, otherwise use the one provided. 68 val transformedCodebase = CodebaseTransformer.transformIfAvailable(codebase) 69 70 test(transformedCodebase) 71 } 72 toStringnull73 override fun toString() = providerName 74 } 75 76 /** 77 * A [ClassResolver] that is backed by a [URLClassLoader]. 78 * 79 * When [resolveClass] is called this will first look in [codebase] to see if the [ClassItem] has 80 * already been loaded, returning it if found. Otherwise, it will look in the [classLoader] to see 81 * if the class exists on the classpath. If it does then it will create a [DefaultClassItem] to 82 * represent it and add it to the [codebase]. Otherwise, it will return `null`. 83 * 84 * The created [DefaultClassItem] is not a complete representation of the class that was found in 85 * the [classLoader]. It is just a placeholder to indicate that it was found, although that may 86 * change in the future. 87 */ 88 class ClassLoaderBasedClassResolver( 89 jars: List<File>, 90 codebaseConfig: Codebase.Config = Codebase.Config.NOOP, 91 ) : ClassResolver { 92 93 private val assembler by 94 lazy(LazyThreadSafetyMode.NONE) { 95 val location = jars.first() 96 TextCodebaseAssembler.createAssembler( 97 location = location, 98 description = "Codebase for resolving classes in $location for tests", 99 codebaseConfig = codebaseConfig, 100 classResolver = null, 101 ) 102 } 103 104 private val codebase by lazy(LazyThreadSafetyMode.NONE) { assembler.codebase } 105 106 private val classLoader by 107 lazy(LazyThreadSafetyMode.NONE) { 108 val urls = jars.map { it.toURI().toURL() }.toTypedArray() 109 URLClassLoader(urls, null) 110 } 111 112 private fun findClassInClassLoader(qualifiedName: String): Class<*>? { 113 var binaryName = qualifiedName 114 do { 115 try { 116 return classLoader.loadClass(binaryName) 117 } catch (e: ClassNotFoundException) { 118 // If the class could not be found then maybe it was a nested class so replace the 119 // last '.' in the name with a $ and try again. If there is no '.' then return. 120 val lastDot = binaryName.lastIndexOf('.') 121 if (lastDot == -1) { 122 return null 123 } else { 124 val before = binaryName.substring(0, lastDot) 125 val after = binaryName.substring(lastDot + 1) 126 binaryName = "$before\$$after" 127 } 128 } 129 } while (true) 130 } 131 132 override fun resolveClass(erasedName: String): ClassItem? { 133 return codebase.findClass(erasedName) 134 ?: run { 135 val cls = findClassInClassLoader(erasedName) ?: return null 136 val packageName = cls.`package`.name 137 138 val itemFactory = assembler.itemFactory 139 140 val packageItem = codebase.findOrCreatePackage(packageName) 141 itemFactory.createClassItem( 142 fileLocation = FileLocation.UNKNOWN, 143 modifiers = createImmutableModifiers(VisibilityLevel.PACKAGE_PRIVATE), 144 classKind = ClassKind.CLASS, 145 containingClass = null, 146 containingPackage = packageItem, 147 qualifiedName = cls.canonicalName, 148 typeParameterList = TypeParameterList.NONE, 149 origin = ClassOrigin.CLASS_PATH, 150 superClassType = null, 151 interfaceTypes = emptyList(), 152 ) 153 } 154 } 155 } 156