Snap for 11414347 from 89ea67b29fc63dceab83cd0660e4930491a57301 to androidx-graphics-release

Change-Id: Iaeb439c85b3814bbc912ff63df4c323aedf9cb66
diff --git a/README.md b/README.md
index f85647a..95d76de 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,7 @@
 
 To run the metalava executable:
 
-### Through Gradle 
+### Through Gradle
 
 To list all the options:
 
diff --git a/buildSrc/src/main/kotlin/com/android/tools/metalava/MetalavaBuildPlugin.kt b/buildSrc/src/main/kotlin/com/android/tools/metalava/MetalavaBuildPlugin.kt
index 7c51297..9fbffce 100644
--- a/buildSrc/src/main/kotlin/com/android/tools/metalava/MetalavaBuildPlugin.kt
+++ b/buildSrc/src/main/kotlin/com/android/tools/metalava/MetalavaBuildPlugin.kt
@@ -80,7 +80,8 @@
     fun configureLint(project: Project) {
         project.apply(mapOf("plugin" to "com.android.lint"))
         project.extensions.getByType<Lint>().apply {
-            fatal.add("UastImplementation")
+            fatal.add("UastImplementation") // go/hide-uast-impl
+            fatal.add("KotlincFE10") // b/239982263
             disable.add("UseTomlInstead") // not useful for this project
             disable.add("GradleDependency") // not useful for this project
             abortOnError = true
diff --git a/gradle.properties b/gradle.properties
index 4709ed8..ec8dfcd 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -10,3 +10,5 @@
 kotlin.incremental.usePreciseJavaTracking=true
 # Needed for the integration project
 android.useAndroidX=true
+# b/271371556
+android.lint.useK2Uast=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index aa6f453..ec3f403 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,9 +1,9 @@
 [versions]
 kotlin = "1.8.21"
-androidLint = "31.4.0-alpha02"
+androidLint = "31.4.0-alpha07"
 
 [libraries]
-androidGradlePlugin = { module = "com.android.tools.build:gradle", version = "8.4.0-alpha02" }
+androidGradlePlugin = { module = "com.android.tools.build:gradle", version = "8.4.0-alpha07" }
 androidLint = { module = "com.android.tools.lint:lint", version.ref = "androidLint" }
 androidLintApi = { module = "com.android.tools.lint:lint-api", version.ref = "androidLint" }
 androidLintChecks = { module = "com.android.tools.lint:lint-checks", version.ref = "androidLint" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 01eff4a..f01754e 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,7 @@
 #Tue May 30 13:39:24 PDT 2023
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
-distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-rc-1-all.zip
+distributionSha256Sum=7f95f484b97c07afc9e4dbca18d9b433155747a462857c7a7620694c6e20a58d
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/ClassType.kt b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/ClassType.kt
deleted file mode 100644
index c28bd14..0000000
--- a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/ClassType.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tools.metalava.model.psi
-
-import com.intellij.psi.PsiClass
-import com.intellij.psi.PsiTypeParameter
-
-internal enum class ClassType {
-    INTERFACE,
-    ENUM,
-    ANNOTATION_TYPE,
-    TYPE_PARAMETER,
-    CLASS;
-
-    companion object {
-        fun getClassType(psiClass: PsiClass): ClassType {
-            return when {
-                psiClass.isAnnotationType -> ANNOTATION_TYPE
-                psiClass.isInterface -> INTERFACE
-                psiClass.isEnum -> ENUM
-                psiClass is PsiTypeParameter -> TYPE_PARAMETER
-                else -> CLASS
-            }
-        }
-    }
-}
diff --git a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiBasedCodebase.kt b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiBasedCodebase.kt
index 1498d6a..02fae16 100644
--- a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiBasedCodebase.kt
+++ b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiBasedCodebase.kt
@@ -28,6 +28,8 @@
 import com.android.tools.metalava.model.MethodItem
 import com.android.tools.metalava.model.PackageItem
 import com.android.tools.metalava.model.PackageList
+import com.android.tools.metalava.model.TypeParameterItem
+import com.android.tools.metalava.model.TypeUse
 import com.android.tools.metalava.model.source.SourceCodebase
 import com.android.tools.metalava.reporter.Issues
 import com.android.tools.metalava.reporter.Reporter
@@ -51,6 +53,7 @@
 import com.intellij.psi.PsiPackage
 import com.intellij.psi.PsiSubstitutor
 import com.intellij.psi.PsiType
+import com.intellij.psi.PsiTypeParameter
 import com.intellij.psi.TypeAnnotationProvider
 import com.intellij.psi.javadoc.PsiDocComment
 import com.intellij.psi.search.GlobalSearchScope
@@ -564,15 +567,6 @@
             }
         }
 
-        if (classItem.classType == ClassType.TYPE_PARAMETER) {
-            // Don't put PsiTypeParameter classes into the registry; e.g. when we're visiting
-            //  java.util.stream.Stream<R>
-            // we come across "R" and would try to place it here.
-            classItem.containingPackage = emptyPackage
-            classItem.finishInitialization()
-            return classItem
-        }
-
         // TODO: Cache for adjacent files!
         val packageName = getPackageName(clz)
         registerPackageClass(packageName, classItem)
@@ -619,12 +613,21 @@
         return classMap[className]
     }
 
+    override fun resolveClass(className: String): ClassItem? = findOrCreateClass(className)
+
     open fun findClass(psiClass: PsiClass): PsiClassItem? {
         val qualifiedName: String = psiClass.qualifiedName ?: psiClass.name!!
         return classMap[qualifiedName]
     }
 
     internal fun findOrCreateClass(qualifiedName: String): PsiClassItem? {
+        // Check to see if the class has already been seen and if so return it immediately.
+        findClass(qualifiedName)?.let {
+            return it
+        }
+
+        // The following cannot find a class whose name does not correspond to the file name, e.g.
+        // in Java a class that is a second top level class.
         val finder = JavaPsiFacade.getInstance(project)
         val psiClass =
             finder.findClass(qualifiedName, GlobalSearchScope.allScope(project)) ?: return null
@@ -632,6 +635,12 @@
     }
 
     internal fun findOrCreateClass(psiClass: PsiClass): PsiClassItem {
+        if (psiClass is PsiTypeParameter) {
+            error(
+                "Must not be called with PsiTypeParameter; call findOrCreateTypeParameter(...) instead"
+            )
+        }
+
         val existing = findClass(psiClass)
         if (existing != null) {
             return existing
@@ -680,6 +689,34 @@
         return null
     }
 
+    /**
+     * Find a [PsiTypeParameterItem] representing [PsiTypeParameter].
+     *
+     * The corresponding [TypeParameterItem] must always exist, otherwise the source code has
+     * serious syntactic and/or semantic errors.
+     */
+    internal fun findTypeParameter(psiTypeParameter: PsiTypeParameter): TypeParameterItem {
+        // Find the [TypeParameterListOwner] of the type parameter by searching for the
+        // [MethodItem]/[ClassItem] corresponding to the underlying [PsiTypeParameter]'s owner.
+        val psiOwner = psiTypeParameter.owner
+        val typeParameterListOwner =
+            when (psiOwner) {
+                is PsiMethod -> findMethod(psiOwner)
+                is PsiClass -> findClass(psiOwner)
+                else -> null
+            }
+                ?: error("Could not find or recognize owner $psiOwner")
+
+        // Search through the owner's [TypeParameterList] to find the parameter with the matching
+        // name and return that.
+        val typeParameterList = typeParameterListOwner.typeParameterList()
+        val name = psiTypeParameter.name
+        return typeParameterList.typeParameters().firstOrNull { it.name() == name }
+            ?: error(
+                "Could not find type parameter $name in $typeParameterList of $typeParameterListOwner"
+            )
+    }
+
     internal fun getClassType(cls: PsiClass): PsiClassType =
         getFactory().createType(cls, PsiSubstitutor.EMPTY)
 
@@ -687,10 +724,22 @@
         getFactory().createDocCommentFromText(string, parent)
 
     /**
+     * Creates a [PsiClassTypeItem] that is suitable for use as a super type, e.g. in an `extends`
+     * or `implements` list.
+     */
+    internal fun getSuperType(psiType: PsiType): PsiClassTypeItem {
+        return getType(psiType, typeUse = TypeUse.SUPER_TYPE) as PsiClassTypeItem
+    }
+
+    /**
      * Returns a [PsiTypeItem] representing the [psiType]. The [context] is used to get nullability
      * information for Kotlin types.
      */
-    internal fun getType(psiType: PsiType, context: PsiElement? = null): PsiTypeItem {
+    internal fun getType(
+        psiType: PsiType,
+        context: PsiElement? = null,
+        typeUse: TypeUse = TypeUse.GENERAL
+    ): PsiTypeItem {
         val kotlinTypeInfo =
             if (context != null && isKotlin(context)) {
                 KotlinTypeInfo.fromContext(context)
@@ -703,7 +752,7 @@
         // for some type comparisons (and we sometimes end up with unexpected results,
         // e.g. where we fetch an "equals" type from the map but its representation
         // is slightly different than we intended
-        return PsiTypeItem.create(this, psiType, kotlinTypeInfo)
+        return PsiTypeItem.create(this, psiType, kotlinTypeInfo, typeUse)
     }
 
     internal fun getType(psiClass: PsiClass): PsiTypeItem {
diff --git a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiClassItem.kt b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiClassItem.kt
index 3b595da..32dd2d9 100644
--- a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiClassItem.kt
+++ b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiClassItem.kt
@@ -19,13 +19,14 @@
 import com.android.tools.metalava.model.AnnotationItem
 import com.android.tools.metalava.model.AnnotationRetention
 import com.android.tools.metalava.model.ClassItem
+import com.android.tools.metalava.model.ClassKind
+import com.android.tools.metalava.model.ClassTypeItem
 import com.android.tools.metalava.model.ConstructorItem
 import com.android.tools.metalava.model.FieldItem
 import com.android.tools.metalava.model.MethodItem
 import com.android.tools.metalava.model.PackageItem
 import com.android.tools.metalava.model.PropertyItem
 import com.android.tools.metalava.model.SourceFile
-import com.android.tools.metalava.model.TypeItem
 import com.android.tools.metalava.model.TypeParameterList
 import com.android.tools.metalava.model.VisibilityLevel
 import com.android.tools.metalava.model.hasAnnotation
@@ -53,7 +54,7 @@
     private val fullName: String,
     private val qualifiedName: String,
     private val hasImplicitDefaultConstructor: Boolean,
-    internal val classType: ClassType,
+    override val classKind: ClassKind,
     modifiers: PsiModifierItem,
     documentation: String,
     /** True if this class is from the class path (dependencies). Exposed in [isFromClassPath]. */
@@ -82,12 +83,6 @@
 
     override fun isDefined(): Boolean = codebase.unsupported()
 
-    override fun isInterface(): Boolean = classType == ClassType.INTERFACE
-
-    override fun isAnnotationType(): Boolean = classType == ClassType.ANNOTATION_TYPE
-
-    override fun isEnum(): Boolean = classType == ClassType.ENUM
-
     override fun psi() = psiClass
 
     override fun isFromClassPath(): Boolean = fromClassPath
@@ -95,11 +90,11 @@
     override fun hasImplicitDefaultConstructor(): Boolean = hasImplicitDefaultConstructor
 
     private var superClass: ClassItem? = null
-    private var superClassType: TypeItem? = null
+    private var superClassType: ClassTypeItem? = null
 
     override fun superClass(): ClassItem? = superClass
 
-    override fun superClassType(): TypeItem? = superClassType
+    override fun superClassType(): ClassTypeItem? = superClassType
 
     override var stubConstructor: ConstructorItem? = null
     override var artifact: String? = null
@@ -110,13 +105,9 @@
 
     override var hasPrivateConstructor: Boolean = false
 
-    override fun interfaceTypes(): List<TypeItem> = interfaceTypes
+    override fun interfaceTypes(): List<ClassTypeItem> = interfaceTypes
 
-    override fun setInterfaceTypes(interfaceTypes: List<TypeItem>) {
-        @Suppress("UNCHECKED_CAST") setInterfaces(interfaceTypes as List<PsiTypeItem>)
-    }
-
-    private fun setInterfaces(interfaceTypes: List<PsiTypeItem>) {
+    override fun setInterfaceTypes(interfaceTypes: List<ClassTypeItem>) {
         this.interfaceTypes = interfaceTypes
     }
 
@@ -159,9 +150,9 @@
     }
 
     private lateinit var innerClasses: List<PsiClassItem>
-    private lateinit var interfaceTypes: List<TypeItem>
+    private lateinit var interfaceTypes: List<ClassTypeItem>
     private lateinit var constructors: List<PsiConstructorItem>
-    private lateinit var methods: List<PsiMethodItem>
+    private lateinit var methods: MutableList<PsiMethodItem>
     private lateinit var properties: List<PsiPropertyItem>
     private lateinit var fields: List<FieldItem>
 
@@ -185,25 +176,15 @@
     final override var primaryConstructor: PsiConstructorItem? = null
         private set
 
-    override fun toType(): TypeItem {
-        return codebase.getType(codebase.getClassType(psiClass))
-    }
+    override fun type(): ClassTypeItem =
+        codebase.getType(codebase.getClassType(psiClass)) as ClassTypeItem
 
     override fun hasTypeVariables(): Boolean = psiClass.hasTypeParameters()
 
-    override fun typeParameterList(): TypeParameterList {
-        if (psiClass.hasTypeParameters()) {
-            return PsiTypeParameterList(
-                codebase,
-                psiClass.typeParameterList ?: return TypeParameterList.NONE
-            )
-        } else {
-            return TypeParameterList.NONE
-        }
-    }
+    private val typeParameterList: TypeParameterList by
+        lazy(LazyThreadSafetyMode.NONE) { PsiTypeParameterList.create(codebase, psiClass) }
 
-    override val isTypeParameter: Boolean
-        get() = psiClass is PsiTypeParameter
+    override fun typeParameterList() = typeParameterList
 
     override fun getSourceFile(): SourceFile? {
         if (isInnerClass()) {
@@ -270,18 +251,18 @@
         // Map them to PsiTypeItems.
         val interfaceTypes =
             interfaces.map {
-                val type = codebase.getType(it)
-                // ensure that we initialize classes eagerly too, so that they're registered etc
-                type.asClass()
-                type
+                codebase.getSuperType(it).also { type ->
+                    // ensure that we initialize classes eagerly too, so that they're registered etc
+                    type.asClass()
+                }
             }
-        setInterfaces(interfaceTypes)
+        setInterfaceTypes(interfaceTypes)
 
         if (!isInterface) {
             // Set the super class type for classes
             val superClassPsiType = psiClass.superClassType as? PsiType
             superClassPsiType?.let { superType ->
-                this.superClassType = codebase.getType(superType)
+                this.superClassType = codebase.getSuperType(superType)
                 this.superClass = this.superClassType?.asClass()
             }
         }
@@ -291,20 +272,6 @@
         }
     }
 
-    internal fun initialize(
-        innerClasses: List<PsiClassItem>,
-        interfaceTypes: List<TypeItem>,
-        constructors: List<PsiConstructorItem>,
-        methods: List<PsiMethodItem>,
-        fields: List<FieldItem>
-    ) {
-        this.innerClasses = innerClasses
-        this.interfaceTypes = interfaceTypes
-        this.constructors = constructors
-        this.methods = methods
-        this.fields = fields
-    }
-
     override fun equals(other: Any?): Boolean {
         if (this === other) {
             return true
@@ -321,19 +288,7 @@
         val method = template as PsiMethodItem
         val newMethod = PsiMethodItem.create(codebase, this, method)
 
-        if (template.throwsTypes().isEmpty()) {
-            newMethod.setThrowsTypes(emptyList())
-        } else {
-            val throwsTypes = mutableListOf<ClassItem>()
-            for (type in template.throwsTypes()) {
-                if (type.codebase === codebase) {
-                    throwsTypes.add(type)
-                } else {
-                    throwsTypes.add(codebase.findOrCreateClass(((type as PsiClassItem).psiClass)))
-                }
-            }
-            newMethod.setThrowsTypes(throwsTypes)
-        }
+        newMethod.setThrowsTypes(method.throwsTypes())
         newMethod.finishInitialization()
 
         // Remember which class this method was copied from.
@@ -343,7 +298,7 @@
     }
 
     override fun addMethod(method: MethodItem) {
-        (methods as MutableList<PsiMethodItem>).add(method as PsiMethodItem)
+        methods.add(method as PsiMethodItem)
     }
 
     private var retention: AnnotationRetention? = null
@@ -395,13 +350,15 @@
             fromClassPath: Boolean
         ): PsiClassItem {
             if (psiClass is PsiTypeParameter) {
-                return PsiTypeParameterItem.create(codebase, psiClass)
+                error(
+                    "Must not be called with PsiTypeParameter; use PsiTypeParameterItem.create(...) instead"
+                )
             }
             val simpleName = psiClass.name!!
             val fullName = computeFullClassName(psiClass)
             val qualifiedName = psiClass.qualifiedName ?: simpleName
             val hasImplicitDefaultConstructor = hasImplicitDefaultConstructor(psiClass)
-            val classType = ClassType.getClassType(psiClass)
+            val classKind = getClassKind(psiClass)
 
             val commentText = javadoc(psiClass)
             val modifiers = PsiModifierItem.create(codebase, psiClass, commentText)
@@ -413,7 +370,7 @@
                     name = simpleName,
                     fullName = fullName,
                     qualifiedName = qualifiedName,
-                    classType = classType,
+                    classKind = classKind,
                     hasImplicitDefaultConstructor = hasImplicitDefaultConstructor,
                     documentation = commentText,
                     modifiers = modifiers,
@@ -431,7 +388,7 @@
             val isKotlin = isKotlin(psiClass)
 
             if (
-                classType == ClassType.ANNOTATION_TYPE &&
+                classKind == ClassKind.ANNOTATION_TYPE &&
                     !hasExplicitRetention(modifiers, psiClass, isKotlin)
             ) {
                 // By policy, include explicit retention policy annotation if missing
@@ -484,7 +441,7 @@
                     } else {
                         constructors.add(constructor)
                     }
-                } else if (classType == ClassType.ENUM && psiMethod is SyntheticElement) {
+                } else if (classKind == ClassKind.ENUM && psiMethod is SyntheticElement) {
                     // skip
                 } else {
                     val method = PsiMethodItem.create(codebase, item, psiMethod)
@@ -519,7 +476,7 @@
                 psiFields.asSequence().mapTo(fields) { PsiFieldItem.create(codebase, item, it) }
             }
 
-            if (classType == ClassType.INTERFACE) {
+            if (classKind == ClassKind.INTERFACE) {
                 // All members are implicitly public, fields are implicitly static, non-static
                 // methods are abstract
                 // (except in Java 1.9, where they can be private
@@ -611,6 +568,17 @@
             return item
         }
 
+        internal fun getClassKind(psiClass: PsiClass): ClassKind {
+            return when {
+                psiClass.isAnnotationType -> ClassKind.ANNOTATION_TYPE
+                psiClass.isInterface -> ClassKind.INTERFACE
+                psiClass.isEnum -> ClassKind.ENUM
+                psiClass is PsiTypeParameter ->
+                    error("Must not call this with a PsiTypeParameter - $psiClass")
+                else -> ClassKind.CLASS
+            }
+        }
+
         /**
          * Computes the "full" class name; this is not the qualified class name (e.g. with package)
          * but for an inner class it includes all the outer classes
diff --git a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt
index 2cb2b43..424ea98 100644
--- a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt
+++ b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt
@@ -18,11 +18,13 @@
 
 import com.android.tools.metalava.model.ClassItem
 import com.android.tools.metalava.model.MethodItem
+import com.android.tools.metalava.model.ThrowableType
 import com.android.tools.metalava.model.TypeItem
 import com.android.tools.metalava.model.TypeParameterList
 import com.android.tools.metalava.model.computeSuperMethods
 import com.intellij.psi.PsiAnnotationMethod
 import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiTypeParameter
 import org.jetbrains.kotlin.name.JvmStandardClassIds
 import org.jetbrains.kotlin.psi.KtFunction
 import org.jetbrains.kotlin.psi.KtNamedFunction
@@ -124,25 +126,19 @@
         return superMethods!!
     }
 
-    override fun typeParameterList(): TypeParameterList {
-        if (psiMethod.hasTypeParameters()) {
-            return PsiTypeParameterList(
-                codebase,
-                psiMethod.typeParameterList ?: return TypeParameterList.NONE
-            )
-        } else {
-            return TypeParameterList.NONE
-        }
-    }
+    private val typeParameterList: TypeParameterList by
+        lazy(LazyThreadSafetyMode.NONE) { PsiTypeParameterList.create(codebase, psiMethod) }
+
+    override fun typeParameterList() = typeParameterList
 
     //    private var throwsTypes: List<ClassItem>? = null
-    private lateinit var throwsTypes: List<ClassItem>
+    private lateinit var throwsTypes: List<ThrowableType>
 
-    internal fun setThrowsTypes(throwsTypes: List<ClassItem>) {
+    internal fun setThrowsTypes(throwsTypes: List<ThrowableType>) {
         this.throwsTypes = throwsTypes
     }
 
-    override fun throwsTypes(): List<ClassItem> = throwsTypes
+    override fun throwsTypes(): List<ThrowableType> = throwsTypes
 
     override fun isExtensionMethod(): Boolean {
         if (isKotlin()) {
@@ -366,8 +362,8 @@
             containingClass: PsiClassItem,
             original: PsiMethodItem
         ): PsiMethodItem {
-            val replacementMap = containingClass.mapTypeVariables(original.containingClass())
-            val returnType = original.returnType.convertType(replacementMap) as PsiTypeItem
+            val typeParameterBindings = containingClass.mapTypeVariables(original.containingClass())
+            val returnType = original.returnType.convertType(typeParameterBindings) as PsiTypeItem
 
             // This results in a PsiMethodItem that is inconsistent, compared with other
             // PsiMethodItem. PsiMethodItems created directly from the source are such that:
@@ -396,7 +392,11 @@
                     modifiers = PsiModifierItem.create(codebase, original.modifiers),
                     returnType = returnType,
                     parameters =
-                        PsiParameterItem.create(codebase, original.parameters(), replacementMap)
+                        PsiParameterItem.create(
+                            codebase,
+                            original.parameters(),
+                            typeParameterBindings
+                        )
                 )
             method.modifiers.setOwner(method)
 
@@ -418,24 +418,33 @@
             }
         }
 
-        private fun throwsTypes(codebase: PsiBasedCodebase, psiMethod: PsiMethod): List<ClassItem> {
-            val interfaces = psiMethod.throwsList.referencedTypes
-            if (interfaces.isEmpty()) {
+        private fun throwsTypes(
+            codebase: PsiBasedCodebase,
+            psiMethod: PsiMethod
+        ): List<ThrowableType> {
+            val throwsClassTypes = psiMethod.throwsList.referencedTypes
+            if (throwsClassTypes.isEmpty()) {
                 return emptyList()
             }
 
-            val result = ArrayList<ClassItem>(interfaces.size)
-            for (cls in interfaces) {
-                result.add(codebase.findClass(cls) ?: continue)
-            }
-
-            // We're sorting the names here even though outputs typically do their own sorting,
-            // since for example the MethodItem.sameSignature check wants to do an
-            // element-by-element
-            // comparison to see if the signature matches, and that should match overrides even if
-            // they specify their elements in different orders.
-            result.sortWith(ClassItem.fullNameComparator)
-            return result
+            return throwsClassTypes
+                // Resolve the type to a PsiClass, may return null.
+                .mapNotNull { psiType -> psiType.resolve() }
+                // Find or create a PsiClassItem or PsiTypeParameterItem for the underlying
+                // PsiClass.
+                .map { throwsClass ->
+                    // PsiTypeParameterItem have to be created separately to PsiClassItem.
+                    if (throwsClass is PsiTypeParameter) {
+                        ThrowableType.ofTypeParameter(codebase.findTypeParameter(throwsClass))
+                    } else {
+                        ThrowableType.ofClass(codebase.findOrCreateClass(throwsClass))
+                    }
+                }
+                // We're sorting the names here even though outputs typically do their own sorting,
+                // since for example the MethodItem.sameSignature check wants to do an
+                // element-by-element comparison to see if the signature matches, and that should
+                // match overrides even if they specify their elements in different orders.
+                .sortedWith(ThrowableType.fullNameComparator)
         }
     }
 
diff --git a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiModifierItem.kt b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiModifierItem.kt
index 7a18b9f..417fcba 100644
--- a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiModifierItem.kt
+++ b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiModifierItem.kt
@@ -361,10 +361,11 @@
             codebase: PsiBasedCodebase,
             element: PsiModifierListOwner
         ): PsiModifierItem {
-            val modifierList = element.modifierList ?: return PsiModifierItem(codebase)
-            var flags = computeFlag(element, modifierList)
+            var flags =
+                element.modifierList?.let { modifierList -> computeFlag(element, modifierList) }
+                    ?: PACKAGE_PRIVATE
 
-            val psiAnnotations = modifierList.annotations
+            val psiAnnotations = element.annotations
             return if (psiAnnotations.isEmpty()) {
                 PsiModifierItem(codebase, flags)
             } else {
diff --git a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiParameterItem.kt b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiParameterItem.kt
index e07be58..ca3c0c6 100644
--- a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiParameterItem.kt
+++ b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiParameterItem.kt
@@ -20,6 +20,7 @@
 import com.android.tools.metalava.model.MethodItem
 import com.android.tools.metalava.model.ParameterItem
 import com.android.tools.metalava.model.TypeItem
+import com.android.tools.metalava.model.TypeParameterBindings
 import com.android.tools.metalava.model.VisibilityLevel
 import com.android.tools.metalava.model.findAnnotation
 import com.android.tools.metalava.model.hasAnnotation
@@ -28,6 +29,7 @@
 import com.intellij.psi.PsiArrayType
 import com.intellij.psi.PsiEllipsisType
 import com.intellij.psi.PsiParameter
+import com.intellij.psi.impl.compiled.ClsParameterImpl
 import org.jetbrains.kotlin.analysis.api.analyze
 import org.jetbrains.kotlin.analysis.api.symbols.KtFunctionLikeSymbol
 import org.jetbrains.kotlin.analysis.api.symbols.KtFunctionSymbol
@@ -100,7 +102,11 @@
             }
 
             // Parameter names from classpath jars are not present as annotations
-            if (isFromClassPath()) {
+            if (
+                isFromClassPath() &&
+                    (psiParameter is ClsParameterImpl) &&
+                    !psiParameter.isAutoGeneratedName
+            ) {
                 return name()
             }
         }
@@ -360,9 +366,9 @@
         fun create(
             codebase: PsiBasedCodebase,
             original: PsiParameterItem,
-            replacementMap: Map<TypeItem, TypeItem>
+            typeParameterBindings: TypeParameterBindings
         ): PsiParameterItem {
-            val type = original.type.convertType(replacementMap) as PsiTypeItem
+            val type = original.type.convertType(typeParameterBindings) as PsiTypeItem
             val parameter =
                 PsiParameterItem(
                     codebase = codebase,
@@ -380,9 +386,9 @@
         fun create(
             codebase: PsiBasedCodebase,
             original: List<ParameterItem>,
-            replacementMap: Map<TypeItem, TypeItem>
+            typeParameterBindings: TypeParameterBindings
         ): List<PsiParameterItem> {
-            return original.map { create(codebase, it as PsiParameterItem, replacementMap) }
+            return original.map { create(codebase, it as PsiParameterItem, typeParameterBindings) }
         }
 
         private fun createParameterModifiers(
diff --git a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiSourceParser.kt b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiSourceParser.kt
index 2ccd287..6e1bb6b 100644
--- a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiSourceParser.kt
+++ b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiSourceParser.kt
@@ -19,21 +19,25 @@
 import com.android.SdkConstants
 import com.android.tools.lint.UastEnvironment
 import com.android.tools.lint.annotations.Extractor
-import com.android.tools.lint.checks.infrastructure.ClassName
+import com.android.tools.lint.computeMetadata
 import com.android.tools.lint.detector.api.Project
 import com.android.tools.metalava.model.AnnotationManager
 import com.android.tools.metalava.model.ClassResolver
 import com.android.tools.metalava.model.noOpAnnotationManager
 import com.android.tools.metalava.model.source.DEFAULT_JAVA_LANGUAGE_LEVEL
+import com.android.tools.metalava.model.source.DEFAULT_KOTLIN_LANGUAGE_LEVEL
 import com.android.tools.metalava.model.source.SourceCodebase
 import com.android.tools.metalava.model.source.SourceParser
-import com.android.tools.metalava.reporter.Issues
+import com.android.tools.metalava.model.source.SourceSet
+import com.android.tools.metalava.model.source.utils.OVERVIEW_HTML
+import com.android.tools.metalava.model.source.utils.PACKAGE_HTML
+import com.android.tools.metalava.model.source.utils.findPackage
 import com.android.tools.metalava.reporter.Reporter
 import com.intellij.pom.java.LanguageLevel
 import java.io.File
-import java.nio.file.Files
 import org.jetbrains.kotlin.config.ApiVersion
 import org.jetbrains.kotlin.config.JVMConfigurationKeys
+import org.jetbrains.kotlin.config.LanguageFeature
 import org.jetbrains.kotlin.config.LanguageVersion
 import org.jetbrains.kotlin.config.LanguageVersionSettings
 import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl
@@ -80,42 +84,42 @@
      * All supplied [File] objects will be mapped to [File.getAbsoluteFile].
      */
     override fun parseSources(
-        sources: List<File>,
+        sourceSet: SourceSet,
+        commonSourceSet: SourceSet,
         description: String,
-        sourcePath: List<File>,
         classPath: List<File>,
     ): PsiBasedCodebase {
-        val absoluteSources = sources.map { it.absoluteFile }
-
-        val absoluteSourceRoots =
-            sourcePath.filter { it.path.isNotBlank() }.map { it.absoluteFile }.toMutableList()
-
-        // Add in source roots implied by the source files
-        extractRoots(reporter, absoluteSources, absoluteSourceRoots)
-
-        val absoluteClasspath = classPath.map { it.absoluteFile }
-
         return parseAbsoluteSources(
-            absoluteSources,
+            sourceSet.absoluteCopy().extractRoots(reporter),
+            commonSourceSet.absoluteCopy().extractRoots(reporter),
             description,
-            absoluteSourceRoots,
-            absoluteClasspath,
+            classPath.map { it.absoluteFile }
         )
     }
 
     /** Returns a codebase initialized from the given set of absolute files. */
     private fun parseAbsoluteSources(
-        sources: List<File>,
+        sourceSet: SourceSet,
+        commonSourceSet: SourceSet,
         description: String,
-        sourceRoots: List<File>,
         classpath: List<File>,
     ): PsiBasedCodebase {
         val config = UastEnvironment.Configuration.create(useFirUast = useK2Uast)
         config.javaLanguageLevel = javaLanguageLevel
 
-        val rootDir = sourceRoots.firstOrNull() ?: File("").canonicalFile
+        val rootDir = sourceSet.sourcePath.firstOrNull() ?: File("").canonicalFile
 
-        configureUastEnvironment(config, sourceRoots, classpath, rootDir)
+        if (commonSourceSet.sources.isNotEmpty()) {
+            configureUastEnvironmentForKMP(
+                config,
+                sourceSet.sources,
+                commonSourceSet.sources,
+                classpath,
+                rootDir
+            )
+        } else {
+            configureUastEnvironment(config, sourceSet.sourcePath, classpath, rootDir)
+        }
         // K1 UAST: loading of JDK (via compiler config, i.e., only for FE1.0), when using JDK9+
         jdkHome?.let {
             if (isJdkModular(it)) {
@@ -126,11 +130,11 @@
 
         val environment = psiEnvironmentManager.createEnvironment(config)
 
-        val kotlinFiles = sources.filter { it.path.endsWith(SdkConstants.DOT_KT) }
+        val kotlinFiles = sourceSet.sources.filter { it.path.endsWith(SdkConstants.DOT_KT) }
         environment.analyzeFiles(kotlinFiles)
 
-        val units = Extractor.createUnitsForFiles(environment.ideaProject, sources)
-        val packageDocs = gatherPackageJavadoc(sources, sourceRoots)
+        val units = Extractor.createUnitsForFiles(environment.ideaProject, sourceSet.sources)
+        val packageDocs = gatherPackageJavadoc(sourceSet)
 
         val codebase = PsiBasedCodebase(rootDir, description, annotationManager, reporter)
         codebase.initialize(environment, units, packageDocs)
@@ -189,14 +193,137 @@
             ),
         )
     }
+
+    private fun configureUastEnvironmentForKMP(
+        config: UastEnvironment.Configuration,
+        sourceFiles: List<File>,
+        commonSourceFiles: List<File>,
+        classpath: List<File>,
+        rootDir: File,
+    ) {
+        // TODO(b/322111050): consider providing a nice DSL at Lint level
+        val projectXml = File.createTempFile("project", ".xml", rootDir)
+
+        fun describeSources(sources: List<File>) = buildString {
+            for (source in sources) {
+                if (!source.isFile) continue
+                appendLine("    <src file=\"${source.absolutePath}\" />")
+            }
+        }
+
+        fun describeClasspath() = buildString {
+            for (dep in classpath) {
+                // TODO: what other kinds of dependencies?
+                if (dep.extension !in SUPPORTED_CLASSPATH_EXT) continue
+                appendLine("    <classpath ${dep.extension}=\"${dep.absolutePath}\" />")
+            }
+        }
+
+        // We're about to build the description of Lint's project model.
+        // Alas, no proper documentation is available. Please refer to examples at upstream Lint:
+        // https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:lint/libs/lint-tests/src/test/java/com/android/tools/lint/ProjectInitializerTest.kt
+        //
+        // An ideal project structure would look like:
+        //
+        // <project>
+        //   <root dir="frameworks/support/compose/ui/ui"/>
+        //   <module name="commonMain" android="false">
+        //     <src file="src/commonMain/.../file1.kt" /> <!-- and so on -->
+        //     <klib file="lib/if/any.klib" />
+        //     <classpath jar="/path/to/kotlin/coroutinesCore.jar" />
+        //     ...
+        //   </module>
+        //   <module name="jvmMain" android="false">
+        //     <dep module="commonMain" kind="dependsOn" />
+        //     <src file="src/jvmMain/.../file1.kt" /> <!-- and so on -->
+        //     ...
+        //   </module>
+        //   <module name="androidMain" android="true">
+        //     <dep module="jvmMain" kind="dependsOn" />
+        //     <src file="src/androidMain/.../file1.kt" /> <!-- and so on -->
+        //     ...
+        //   </module>
+        //   ...
+        // </project>
+        //
+        // That is, there are common modules where `expect` declarations and common business logic
+        // reside, along with binary dependencies of several formats, including klib and jar.
+        // Then, platform-specific modules "depend" on common modules, and have their own source set
+        // and binary dependencies.
+        //
+        // For now, with --common-source-path, common source files are isolated, but the project
+        // structure is not fully conveyed. Therefore, we will reuse the same binary dependencies
+        // for all modules (which only(?) cause performance degradation on binary resolution).
+        val description = buildString {
+            appendLine("""<?xml version="1.0" encoding="utf-8"?>""")
+            appendLine("<project>")
+            appendLine("  <root dir=\"${rootDir.absolutePath}\" />")
+            appendLine("  <module name=\"commonMain\" android=\"false\" >")
+            append(describeSources(commonSourceFiles))
+            append(describeClasspath())
+            appendLine("  </module>")
+            appendLine("  <module name=\"app\" >")
+            appendLine("    <dep module=\"commonMain\" kind=\"dependsOn\" />")
+            // NB: While K2 requires separate common / platform-specific source roots, K1 still
+            // needs to receive all source roots at once. Thus, existing usages (e.g., androidx)
+            // often pass all source files, according to compiler configuration.
+            // To make a correct module structure, we need to filter out common source files here.
+            // TODO: once fully switching to K2 and androidx usage is adjusted, we won't need this.
+            append(describeSources(sourceFiles - commonSourceFiles))
+            append(describeClasspath())
+            appendLine("  </module>")
+            appendLine("</project>")
+        }
+        projectXml.writeText(description)
+
+        // TODO: use Lint's [withKMPEnabled] util when available
+        val languageLevel = LanguageVersion.fromVersionString(DEFAULT_KOTLIN_LANGUAGE_LEVEL)!!
+        val apiVersion = ApiVersion.createByLanguageVersion(languageLevel)
+        val kotlinLanguageLevel =
+            LanguageVersionSettingsImpl(
+                languageLevel,
+                apiVersion,
+                emptyMap(),
+                mapOf(LanguageFeature.MultiPlatformProjects to LanguageFeature.State.ENABLED),
+            )
+        val lintClient = MetalavaCliClient(kotlinLanguageLevel)
+        // This will parse the description of Lint's project model and populate the module structure
+        // inside the given Lint client. We will use it to set up the project structure that
+        // [UastEnvironment] requires, which in turn uses that to set up Kotlin compiler frontend.
+        // The overall flow looks like:
+        //   project.xml -> Lint Project model -> UastEnvironment Module -> Kotlin compiler FE / AA
+        // There are a couple of limitations that force use fall into this long steps:
+        //  * Lint Project creation is not exposed at all. Only project.xml parsing is available.
+        //  * UastEnvironment Module simply reuses existing Lint Project model.
+        computeMetadata(lintClient, projectXml)
+        config.addModules(
+            lintClient.knownProjects.map { module ->
+                UastEnvironment.Module(
+                    module,
+                    // K2 UAST: building KtSdkModule for JDK
+                    jdkHome,
+                    includeTests = false,
+                    includeTestFixtureSources = false,
+                    isUnitTest = false
+                )
+            }
+        )
+    }
+
+    companion object {
+        private const val AAR = "aar"
+        private const val JAR = "jar"
+        private const val KLIB = "klib"
+        private val SUPPORTED_CLASSPATH_EXT = listOf(AAR, JAR, KLIB)
+    }
 }
 
-private fun gatherPackageJavadoc(sources: List<File>, sourceRoots: List<File>): PackageDocs {
+private fun gatherPackageJavadoc(sourceSet: SourceSet): PackageDocs {
     val packageComments = HashMap<String, String>(100)
     val overviewHtml = HashMap<String, String>(10)
     val hiddenPackages = HashSet<String>(100)
-    val sortedSourceRoots = sourceRoots.sortedBy { -it.name.length }
-    for (file in sources) {
+    val sortedSourceRoots = sourceSet.sourcePath.sortedBy { -it.name.length }
+    for (file in sourceSet.sources) {
         var javadoc = false
         val map =
             when (file.name) {
@@ -237,114 +364,3 @@
 
     return PackageDocs(packageComments, overviewHtml, hiddenPackages)
 }
-
-const val PACKAGE_HTML = "package.html"
-const val OVERVIEW_HTML = "overview.html"
-
-private fun skippableDirectory(file: File): Boolean =
-    file.path.endsWith(".git") && file.name == ".git"
-
-private fun addSourceFiles(reporter: Reporter, list: MutableList<File>, file: File) {
-    if (file.isDirectory) {
-        if (skippableDirectory(file)) {
-            return
-        }
-        if (Files.isSymbolicLink(file.toPath())) {
-            reporter.report(
-                Issues.IGNORING_SYMLINK,
-                file,
-                "Ignoring symlink during source file discovery directory traversal"
-            )
-            return
-        }
-        val files = file.listFiles()
-        if (files != null) {
-            for (child in files) {
-                addSourceFiles(reporter, list, child)
-            }
-        }
-    } else if (file.isFile) {
-        when {
-            file.name.endsWith(SdkConstants.DOT_JAVA) ||
-                file.name.endsWith(SdkConstants.DOT_KT) ||
-                file.name.equals(PACKAGE_HTML) ||
-                file.name.equals(OVERVIEW_HTML) -> list.add(file)
-        }
-    }
-}
-
-fun gatherSources(reporter: Reporter, sourcePath: List<File>): List<File> {
-    val sources = mutableListOf<File>()
-    for (file in sourcePath) {
-        if (file.path.isBlank()) {
-            // --source-path "" means don't search source path; use "." for pwd
-            continue
-        }
-        addSourceFiles(reporter, sources, file.absoluteFile)
-    }
-    return sources.sortedWith(compareBy { it.name })
-}
-
-fun extractRoots(
-    reporter: Reporter,
-    sources: List<File>,
-    sourceRoots: MutableList<File> = mutableListOf()
-): List<File> {
-    // Cache for each directory since computing root for a source file is
-    // expensive
-    val dirToRootCache = mutableMapOf<String, File>()
-    for (file in sources) {
-        val parent = file.parentFile ?: continue
-        val found = dirToRootCache[parent.path]
-        if (found != null) {
-            continue
-        }
-
-        val root = findRoot(reporter, file) ?: continue
-        dirToRootCache[parent.path] = root
-
-        if (!sourceRoots.contains(root)) {
-            sourceRoots.add(root)
-        }
-    }
-
-    return sourceRoots
-}
-
-/**
- * If given a full path to a Java or Kotlin source file, produces the path to the source root if
- * possible.
- */
-private fun findRoot(reporter: Reporter, file: File): File? {
-    val path = file.path
-    if (path.endsWith(SdkConstants.DOT_JAVA) || path.endsWith(SdkConstants.DOT_KT)) {
-        val pkg = findPackage(file) ?: return null
-        val parent = file.parentFile ?: return null
-        val endIndex = parent.path.length - pkg.length
-        val before = path[endIndex - 1]
-        if (before == '/' || before == '\\') {
-            return File(path.substring(0, endIndex))
-        } else {
-            reporter.report(
-                Issues.IO_ERROR,
-                file,
-                "Unable to determine the package name. " +
-                    "This usually means that a source file was where the directory does not seem to match the package " +
-                    "declaration; we expected the path $path to end with /${pkg.replace('.', '/') + '/' + file.name}"
-            )
-        }
-    }
-
-    return null
-}
-
-/** Finds the package of the given Java/Kotlin source file, if possible */
-fun findPackage(file: File): String? {
-    val source = file.readText(Charsets.UTF_8)
-    return findPackage(source)
-}
-
-/** Finds the package of the given Java/Kotlin source code, if possible */
-fun findPackage(source: String): String? {
-    return ClassName(source).packageName
-}
diff --git a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiTypeItem.kt b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiTypeItem.kt
index b49b6e1..6359bc0 100644
--- a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiTypeItem.kt
+++ b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiTypeItem.kt
@@ -23,9 +23,12 @@
 import com.android.tools.metalava.model.MemberItem
 import com.android.tools.metalava.model.MethodItem
 import com.android.tools.metalava.model.PrimitiveTypeItem
+import com.android.tools.metalava.model.ReferenceTypeItem
+import com.android.tools.metalava.model.TypeArgumentTypeItem
 import com.android.tools.metalava.model.TypeItem
 import com.android.tools.metalava.model.TypeNullability
 import com.android.tools.metalava.model.TypeParameterItem
+import com.android.tools.metalava.model.TypeUse
 import com.android.tools.metalava.model.VariableTypeItem
 import com.android.tools.metalava.model.WildcardTypeItem
 import com.intellij.psi.PsiArrayType
@@ -43,24 +46,8 @@
 import java.lang.IllegalStateException
 
 /** Represents a type backed by PSI */
-sealed class PsiTypeItem(open val codebase: PsiBasedCodebase, open val psiType: PsiType) :
+sealed class PsiTypeItem(val codebase: PsiBasedCodebase, val psiType: PsiType) :
     DefaultTypeItem(codebase) {
-    private var asClass: PsiClassItem? = null
-
-    override fun asClass(): PsiClassItem? {
-        if (this is PrimitiveTypeItem) {
-            return null
-        }
-        if (asClass == null) {
-            asClass = codebase.findClass(psiType)
-        }
-        return asClass
-    }
-
-    override fun hasTypeArguments(): Boolean {
-        val type = psiType
-        return type is PsiClassType && type.hasParameters()
-    }
 
     /** Returns `true` if `this` type can be assigned from `other` without unboxing the other. */
     fun isAssignableFromWithoutUnboxing(other: PsiTypeItem): Boolean {
@@ -121,19 +108,44 @@
         internal fun create(
             codebase: PsiBasedCodebase,
             psiType: PsiType,
-            kotlinType: KotlinTypeInfo?
+            kotlinType: KotlinTypeInfo?,
+            typeUse: TypeUse = TypeUse.GENERAL,
         ): PsiTypeItem {
             return when (psiType) {
-                is PsiPrimitiveType -> PsiPrimitiveTypeItem(codebase, psiType, kotlinType)
-                is PsiArrayType -> PsiArrayTypeItem(codebase, psiType, kotlinType)
+                is PsiPrimitiveType ->
+                    PsiPrimitiveTypeItem.create(
+                        codebase = codebase,
+                        psiType = psiType,
+                        kotlinType = kotlinType,
+                    )
+                is PsiArrayType ->
+                    PsiArrayTypeItem.create(
+                        codebase = codebase,
+                        psiType = psiType,
+                        kotlinType = kotlinType,
+                    )
                 is PsiClassType -> {
                     if (psiType.resolve() is PsiTypeParameter) {
-                        PsiVariableTypeItem(codebase, psiType, kotlinType)
+                        PsiVariableTypeItem.create(
+                            codebase = codebase,
+                            psiType = psiType,
+                            kotlinType = kotlinType,
+                        )
                     } else {
-                        PsiClassTypeItem(codebase, psiType, kotlinType)
+                        PsiClassTypeItem.create(
+                            codebase = codebase,
+                            psiType = psiType,
+                            kotlinType = kotlinType,
+                            typeUse = typeUse,
+                        )
                     }
                 }
-                is PsiWildcardType -> PsiWildcardTypeItem(codebase, psiType, kotlinType)
+                is PsiWildcardType ->
+                    PsiWildcardTypeItem.create(
+                        codebase = codebase,
+                        psiType = psiType,
+                        kotlinType = kotlinType,
+                    )
                 // There are other [PsiType]s, but none can appear in API surfaces.
                 else -> throw IllegalStateException("Invalid type in API surface: $psiType")
             }
@@ -143,12 +155,10 @@
 
 /** A [PsiTypeItem] backed by a [PsiPrimitiveType]. */
 internal class PsiPrimitiveTypeItem(
-    override val codebase: PsiBasedCodebase,
-    override val psiType: PsiPrimitiveType,
-    kotlinType: KotlinTypeInfo? = null,
-    override val kind: PrimitiveTypeItem.Primitive = getKind(psiType),
-    override val modifiers: PsiTypeModifiers =
-        PsiTypeModifiers.create(codebase, psiType, kotlinType)
+    codebase: PsiBasedCodebase,
+    psiType: PsiType,
+    override val kind: PrimitiveTypeItem.Primitive,
+    override val modifiers: PsiTypeModifiers,
 ) : PrimitiveTypeItem, PsiTypeItem(codebase, psiType) {
     override fun duplicate(): PsiPrimitiveTypeItem =
         PsiPrimitiveTypeItem(
@@ -159,6 +169,18 @@
         )
 
     companion object {
+        fun create(
+            codebase: PsiBasedCodebase,
+            psiType: PsiPrimitiveType,
+            kotlinType: KotlinTypeInfo?,
+        ) =
+            PsiPrimitiveTypeItem(
+                codebase = codebase,
+                psiType = psiType,
+                kind = getKind(psiType),
+                modifiers = PsiTypeModifiers.create(codebase, psiType, kotlinType),
+            )
+
         private fun getKind(type: PsiPrimitiveType): PrimitiveTypeItem.Primitive {
             return when (type) {
                 PsiTypes.booleanType() -> PrimitiveTypeItem.Primitive.BOOLEAN
@@ -178,14 +200,11 @@
 
 /** A [PsiTypeItem] backed by a [PsiArrayType]. */
 internal class PsiArrayTypeItem(
-    override val codebase: PsiBasedCodebase,
-    override val psiType: PsiArrayType,
-    kotlinType: KotlinTypeInfo? = null,
-    override val componentType: PsiTypeItem =
-        create(codebase, psiType.componentType, kotlinType?.forArrayComponentType()),
-    override val isVarargs: Boolean = psiType is PsiEllipsisType,
-    override val modifiers: PsiTypeModifiers =
-        PsiTypeModifiers.create(codebase, psiType, kotlinType)
+    codebase: PsiBasedCodebase,
+    psiType: PsiType,
+    override val componentType: PsiTypeItem,
+    override val isVarargs: Boolean,
+    override val modifiers: PsiTypeModifiers,
 ) : ArrayTypeItem, PsiTypeItem(codebase, psiType) {
     override fun duplicate(componentType: TypeItem): ArrayTypeItem =
         PsiArrayTypeItem(
@@ -195,39 +214,79 @@
             isVarargs = isVarargs,
             modifiers = modifiers.duplicate()
         )
+
+    companion object {
+        fun create(
+            codebase: PsiBasedCodebase,
+            psiType: PsiArrayType,
+            kotlinType: KotlinTypeInfo?,
+        ) =
+            PsiArrayTypeItem(
+                codebase = codebase,
+                psiType = psiType,
+                componentType =
+                    create(codebase, psiType.componentType, kotlinType?.forArrayComponentType()),
+                isVarargs = psiType is PsiEllipsisType,
+                modifiers = PsiTypeModifiers.create(codebase, psiType, kotlinType),
+            )
+    }
 }
 
 /** A [PsiTypeItem] backed by a [PsiClassType] that does not represent a type variable. */
 internal class PsiClassTypeItem(
-    override val codebase: PsiBasedCodebase,
-    override val psiType: PsiClassType,
-    kotlinType: KotlinTypeInfo? = null,
-    override val qualifiedName: String = computeQualifiedName(psiType),
-    override val parameters: List<PsiTypeItem> = computeParameters(codebase, psiType, kotlinType),
-    override val outerClassType: PsiClassTypeItem? =
-        computeOuterClass(psiType, codebase, kotlinType),
-    // This should be able to use `psiType.name`, but that sometimes returns null.
-    override val className: String = ClassTypeItem.computeClassName(qualifiedName),
-    override val modifiers: PsiTypeModifiers =
-        PsiTypeModifiers.create(codebase, psiType, kotlinType)
+    codebase: PsiBasedCodebase,
+    psiType: PsiType,
+    override val qualifiedName: String,
+    override val arguments: List<TypeArgumentTypeItem>,
+    override val outerClassType: PsiClassTypeItem?,
+    override val className: String,
+    override val modifiers: PsiTypeModifiers,
 ) : ClassTypeItem, PsiTypeItem(codebase, psiType) {
-    override fun duplicate(outerClass: ClassTypeItem?, parameters: List<TypeItem>): ClassTypeItem =
+
+    private val asClassCache by
+        lazy(LazyThreadSafetyMode.NONE) { codebase.resolveClass(qualifiedName) }
+
+    override fun asClass() = asClassCache
+
+    override fun duplicate(
+        outerClass: ClassTypeItem?,
+        arguments: List<TypeArgumentTypeItem>
+    ): ClassTypeItem =
         PsiClassTypeItem(
             codebase = codebase,
             psiType = psiType,
             qualifiedName = qualifiedName,
-            parameters = parameters.map { it as PsiTypeItem },
+            arguments = arguments,
             outerClassType = outerClass as? PsiClassTypeItem,
             className = className,
             modifiers = modifiers.duplicate()
         )
 
     companion object {
-        private fun computeParameters(
+        fun create(
+            codebase: PsiBasedCodebase,
+            psiType: PsiClassType,
+            kotlinType: KotlinTypeInfo?,
+            typeUse: TypeUse,
+        ): PsiClassTypeItem {
+            val qualifiedName = computeQualifiedName(psiType)
+            return PsiClassTypeItem(
+                codebase = codebase,
+                psiType = psiType,
+                qualifiedName = qualifiedName,
+                arguments = computeTypeArguments(codebase, psiType, kotlinType),
+                outerClassType = computeOuterClass(psiType, codebase, kotlinType),
+                // This should be able to use `psiType.name`, but that sometimes returns null.
+                className = ClassTypeItem.computeClassName(qualifiedName),
+                modifiers = PsiTypeModifiers.create(codebase, psiType, kotlinType, typeUse),
+            )
+        }
+
+        private fun computeTypeArguments(
             codebase: PsiBasedCodebase,
             psiType: PsiClassType,
             kotlinType: KotlinTypeInfo?
-        ): List<PsiTypeItem> {
+        ): List<TypeArgumentTypeItem> {
             val psiParameters =
                 psiType.parameters.toList().ifEmpty {
                     // Sometimes an immediate class type has no parameters even though the class
@@ -239,7 +298,7 @@
                 }
 
             return psiParameters.mapIndexed { i, param ->
-                create(codebase, param, kotlinType?.forParameter(i))
+                create(codebase, param, kotlinType?.forParameter(i)) as TypeArgumentTypeItem
             }
         }
 
@@ -288,15 +347,14 @@
 
 /** A [PsiTypeItem] backed by a [PsiClassType] that represents a type variable.e */
 internal class PsiVariableTypeItem(
-    override val codebase: PsiBasedCodebase,
-    override val psiType: PsiClassType,
-    kotlinType: KotlinTypeInfo? = null,
-    override val name: String = psiType.name,
-    override val modifiers: PsiTypeModifiers =
-        PsiTypeModifiers.create(codebase, psiType, kotlinType),
+    codebase: PsiBasedCodebase,
+    psiType: PsiType,
+    override val name: String,
+    override val modifiers: PsiTypeModifiers,
 ) : VariableTypeItem, PsiTypeItem(codebase, psiType) {
     override val asTypeParameter: TypeParameterItem by lazy {
-        codebase.findClass(psiType) as TypeParameterItem
+        val cls = (psiType as PsiClassType).resolve() ?: error("Could not resolve $psiType")
+        codebase.findTypeParameter(cls as PsiTypeParameter)
     }
 
     override fun duplicate(): PsiVariableTypeItem =
@@ -306,29 +364,52 @@
             name = name,
             modifiers = modifiers.duplicate()
         )
+
+    companion object {
+        fun create(codebase: PsiBasedCodebase, psiType: PsiClassType, kotlinType: KotlinTypeInfo?) =
+            PsiVariableTypeItem(
+                codebase = codebase,
+                psiType = psiType,
+                name = psiType.name,
+                modifiers = PsiTypeModifiers.create(codebase, psiType, kotlinType),
+            )
+    }
 }
 
 /** A [PsiTypeItem] backed by a [PsiWildcardType]. */
 internal class PsiWildcardTypeItem(
-    override val codebase: PsiBasedCodebase,
-    override val psiType: PsiWildcardType,
-    kotlinType: KotlinTypeInfo? = null,
-    override val extendsBound: PsiTypeItem? =
-        createBound(psiType.extendsBound, codebase, kotlinType),
-    override val superBound: PsiTypeItem? = createBound(psiType.superBound, codebase, kotlinType),
-    override val modifiers: PsiTypeModifiers =
-        PsiTypeModifiers.create(codebase, psiType, kotlinType)
+    codebase: PsiBasedCodebase,
+    psiType: PsiType,
+    override val extendsBound: ReferenceTypeItem?,
+    override val superBound: ReferenceTypeItem?,
+    override val modifiers: PsiTypeModifiers,
 ) : WildcardTypeItem, PsiTypeItem(codebase, psiType) {
-    override fun duplicate(extendsBound: TypeItem?, superBound: TypeItem?): WildcardTypeItem =
+    override fun duplicate(
+        extendsBound: ReferenceTypeItem?,
+        superBound: ReferenceTypeItem?
+    ): WildcardTypeItem =
         PsiWildcardTypeItem(
             codebase = codebase,
             psiType = psiType,
-            extendsBound = extendsBound as? PsiTypeItem,
-            superBound = superBound as? PsiTypeItem,
+            extendsBound = extendsBound,
+            superBound = superBound,
             modifiers = modifiers.duplicate()
         )
 
     companion object {
+        fun create(
+            codebase: PsiBasedCodebase,
+            psiType: PsiWildcardType,
+            kotlinType: KotlinTypeInfo?,
+        ) =
+            PsiWildcardTypeItem(
+                codebase = codebase,
+                psiType = psiType,
+                extendsBound = createBound(psiType.extendsBound, codebase, kotlinType),
+                superBound = createBound(psiType.superBound, codebase, kotlinType),
+                modifiers = PsiTypeModifiers.create(codebase, psiType, kotlinType),
+            )
+
         /**
          * If a [PsiWildcardType] doesn't have a bound, the bound is represented as the null
          * [PsiType] instead of just `null`.
@@ -337,12 +418,12 @@
             bound: PsiType,
             codebase: PsiBasedCodebase,
             kotlinType: KotlinTypeInfo?
-        ): PsiTypeItem? {
+        ): ReferenceTypeItem? {
             return if (bound == PsiTypes.nullType()) {
                 null
             } else {
                 // Use the same Kotlin type, because the wildcard isn't its own level in the KtType.
-                create(codebase, bound, kotlinType)
+                create(codebase, bound, kotlinType) as ReferenceTypeItem
             }
         }
     }
diff --git a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiTypeModifiers.kt b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiTypeModifiers.kt
index 36bce58..b1ce51a 100644
--- a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiTypeModifiers.kt
+++ b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiTypeModifiers.kt
@@ -19,6 +19,7 @@
 import com.android.tools.metalava.model.AnnotationItem
 import com.android.tools.metalava.model.TypeModifiers
 import com.android.tools.metalava.model.TypeNullability
+import com.android.tools.metalava.model.TypeUse
 import com.intellij.psi.PsiElement
 import com.intellij.psi.PsiPrimitiveType
 import com.intellij.psi.PsiType
@@ -67,15 +68,17 @@
         fun create(
             codebase: PsiBasedCodebase,
             type: PsiType,
-            kotlinType: KotlinTypeInfo?
+            kotlinType: KotlinTypeInfo?,
+            typeUse: TypeUse = TypeUse.GENERAL,
         ): PsiTypeModifiers {
             val annotations = type.annotations.map { PsiAnnotationItem.create(codebase, it) }
             // Some types have defined nullness, and kotlin types have nullness information.
             // Otherwise, look at the annotations and default to platform nullness.
             val nullability =
-                when (type) {
-                    is PsiPrimitiveType -> TypeNullability.NONNULL
-                    is PsiWildcardType -> TypeNullability.UNDEFINED
+                when {
+                    typeUse == TypeUse.SUPER_TYPE || type is PsiPrimitiveType ->
+                        TypeNullability.NONNULL
+                    type is PsiWildcardType -> TypeNullability.UNDEFINED
                     else -> kotlinType?.nullability()
                             ?: annotations
                                 .firstOrNull { it.isNullnessAnnotation() }
diff --git a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiTypeParameterItem.kt b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiTypeParameterItem.kt
index 16fc65b..69f6fd5 100644
--- a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiTypeParameterItem.kt
+++ b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiTypeParameterItem.kt
@@ -16,8 +16,9 @@
 
 package com.android.tools.metalava.model.psi
 
+import com.android.tools.metalava.model.BoundsTypeItem
 import com.android.tools.metalava.model.TypeParameterItem
-import com.android.tools.metalava.model.psi.ClassType.TYPE_PARAMETER
+import com.android.tools.metalava.model.VariableTypeItem
 import com.intellij.psi.PsiTypeParameter
 import org.jetbrains.kotlin.asJava.elements.KotlinLightTypeParameterBuilder
 import org.jetbrains.kotlin.asJava.elements.KtLightDeclaration
@@ -26,43 +27,61 @@
 
 internal class PsiTypeParameterItem(
     codebase: PsiBasedCodebase,
-    psiClass: PsiTypeParameter,
-    name: String,
+    private val psiClass: PsiTypeParameter,
+    private val name: String,
     modifiers: PsiModifierItem
 ) :
-    PsiClassItem(
+    PsiItem(
         codebase = codebase,
-        psiClass = psiClass,
-        name = name,
-        fullName = name,
-        qualifiedName = name,
-        hasImplicitDefaultConstructor = false,
-        classType = TYPE_PARAMETER,
+        element = psiClass,
         modifiers = modifiers,
         documentation = "",
-        fromClassPath = false
     ),
     TypeParameterItem {
-    override fun typeBounds(): List<PsiTypeItem> = bounds
+
+    override fun name() = name
+
+    override fun type(): VariableTypeItem {
+        return codebase.getType(codebase.getClassType(psiClass)) as VariableTypeItem
+    }
+
+    override fun psi() = psiClass
+
+    override fun typeBounds(): List<BoundsTypeItem> = bounds
 
     override fun isReified(): Boolean {
         return isReified(psiClass as? PsiTypeParameter)
     }
 
-    private lateinit var bounds: List<PsiTypeItem>
+    private lateinit var bounds: List<BoundsTypeItem>
 
     override fun finishInitialization() {
         super.finishInitialization()
 
-        val refs = psiClass.extendsList?.referencedTypes
+        val refs = psiClass.extendsList.referencedTypes
         bounds =
-            if (refs.isNullOrEmpty()) {
+            if (refs.isEmpty()) {
                 emptyList()
             } else {
-                refs.mapNotNull { codebase.getType(it) }
+                refs.mapNotNull { codebase.getType(it) as BoundsTypeItem }
             }
     }
 
+    override fun toString(): String {
+        return String.format("%s [0x%x]", name, System.identityHashCode(this))
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is TypeParameterItem) return false
+
+        return name == other.name()
+    }
+
+    override fun hashCode(): Int {
+        return name.hashCode()
+    }
+
     companion object {
         fun create(codebase: PsiBasedCodebase, psiClass: PsiTypeParameter): PsiTypeParameterItem {
             val simpleName = psiClass.name!!
@@ -76,7 +95,7 @@
                     modifiers = modifiers
                 )
             item.modifiers.setOwner(item)
-            item.initialize(emptyList(), emptyList(), emptyList(), emptyList(), emptyList())
+            item.finishInitialization()
             return item
         }
 
diff --git a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiTypeParameterList.kt b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiTypeParameterList.kt
index cf94917..3a3b930 100644
--- a/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiTypeParameterList.kt
+++ b/metalava-model-psi/src/main/java/com/android/tools/metalava/model/psi/PsiTypeParameterList.kt
@@ -18,6 +18,8 @@
 
 import com.android.tools.metalava.model.DefaultTypeParameterList
 import com.android.tools.metalava.model.TypeParameterItem
+import com.android.tools.metalava.model.TypeParameterList
+import com.intellij.psi.PsiTypeParameterListOwner
 
 internal class PsiTypeParameterList(
     val codebase: PsiBasedCodebase,
@@ -32,4 +34,16 @@
     override fun typeParameters(): List<TypeParameterItem> {
         return typeParameters
     }
+
+    companion object {
+        fun create(codebase: PsiBasedCodebase, psiOwner: PsiTypeParameterListOwner) =
+            if (psiOwner.hasTypeParameters()) {
+                psiOwner.typeParameterList?.let { psiTypeParameterList ->
+                    PsiTypeParameterList(codebase, psiTypeParameterList)
+                }
+                    ?: TypeParameterList.NONE
+            } else {
+                TypeParameterList.NONE
+            }
+    }
 }
diff --git a/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/BasePsiTest.kt b/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/BasePsiTest.kt
index 9c92579..88e2cd2 100644
--- a/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/BasePsiTest.kt
+++ b/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/BasePsiTest.kt
@@ -21,6 +21,7 @@
 import com.android.tools.metalava.model.Codebase
 import com.android.tools.metalava.model.noOpAnnotationManager
 import com.android.tools.metalava.model.source.EnvironmentManager
+import com.android.tools.metalava.model.source.SourceSet
 import com.android.tools.metalava.reporter.BasicReporter
 import com.android.tools.metalava.reporter.Reporter
 import com.android.tools.metalava.testing.TemporaryFolderOwner
@@ -52,6 +53,17 @@
     fun testCodebase(
         vararg sources: TestFile,
         classPath: List<File> = emptyList(),
+        isK2: Boolean = false,
+        action: (Codebase) -> Unit,
+    ) {
+        testCodebase(sources.toList(), emptyList(), classPath, isK2, action)
+    }
+
+    fun testCodebase(
+        sources: List<TestFile>,
+        commonSources: List<TestFile>,
+        classPath: List<File> = emptyList(),
+        isK2: Boolean = false,
         action: (Codebase) -> Unit,
     ) {
         projectDir = temporaryFolder.newFolder()
@@ -62,9 +74,11 @@
                 createTestCodebase(
                     environmentManager,
                     projectDir,
-                    sources.toList(),
+                    sources,
+                    commonSources,
                     classPath,
                     reporter,
+                    isK2,
                 )
             action(codebase)
         }
@@ -85,16 +99,34 @@
         environmentManager: EnvironmentManager,
         directory: File,
         sources: List<TestFile>,
+        commonSources: List<TestFile>,
         classPath: List<File>,
         reporter: Reporter,
+        isK2: Boolean = false,
     ): Codebase {
+        val (sourceDirectory, commonDirectory) =
+            if (commonSources.isEmpty()) {
+                directory to null
+            } else {
+                temporaryFolder.newFolder() to temporaryFolder.newFolder()
+            }
         return environmentManager
-            .createSourceParser(reporter, noOpAnnotationManager)
+            .createSourceParser(reporter, noOpAnnotationManager, useK2Uast = isK2)
             .parseSources(
-                sources = sources.map { it.createFile(directory) },
+                createSourceSet(sources, sourceDirectory),
+                createSourceSet(commonSources, commonDirectory),
                 description = "Test Codebase",
-                sourcePath = listOf(directory),
                 classPath = classPath,
             )
     }
+
+    protected fun createSourceSet(
+        sources: List<TestFile>,
+        sourceDirectory: File?,
+    ): SourceSet {
+        return SourceSet(
+            sources.map { it.createFile(sourceDirectory) },
+            listOfNotNull(sourceDirectory)
+        )
+    }
 }
diff --git a/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiItemTest.kt b/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiItemTest.kt
index 03918a2..7223080 100644
--- a/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiItemTest.kt
+++ b/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiItemTest.kt
@@ -19,7 +19,6 @@
 import com.android.tools.metalava.testing.java
 import kotlin.test.Test
 import kotlin.test.assertEquals
-import kotlin.test.assertNotNull
 
 class PsiItemTest : BasePsiTest() {
     @Test
@@ -49,8 +48,7 @@
                 """,
             )
         ) { codebase ->
-            val testClass = codebase.findClass("test.pkg.Test")
-            assertNotNull(testClass)
+            val testClass = codebase.assertClass("test.pkg.Test")
             val method = testClass.methods().first { it.name().equals("foo") }
             val barJavadoc = "@param bar The bar to foo with\n     *     the thing."
             val bazJavadoc = "@param baz The baz to foo\n     *     I think."
diff --git a/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiMethodItemTest.kt b/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiMethodItemTest.kt
index ee4b496..ba75a06 100644
--- a/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiMethodItemTest.kt
+++ b/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiMethodItemTest.kt
@@ -66,11 +66,11 @@
             """
             )
         testCodebase(codebase) { c ->
-            val ctorItem = c.assertClass("Foo").findMethod("Foo", "")
-            val ctorReturnType = ctorItem!!.returnType()
+            val ctorItem = c.assertClass("Foo").assertMethod("Foo", "")
+            val ctorReturnType = ctorItem.returnType()
 
-            val methodItem = c.assertClass("Foo").findMethod("bar", "")
-            val methodReturnType = methodItem!!.returnType()
+            val methodItem = c.assertClass("Foo").assertMethod("bar", "")
+            val methodReturnType = methodItem.returnType()
 
             assertNotNull(ctorReturnType)
             assertEquals(
diff --git a/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiModifierItemTest.kt b/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiModifierItemTest.kt
index 0fe805a..0856455 100644
--- a/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiModifierItemTest.kt
+++ b/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiModifierItemTest.kt
@@ -21,7 +21,6 @@
 import com.android.tools.metalava.model.Item
 import com.android.tools.metalava.model.PrimitiveTypeItem
 import com.android.tools.metalava.model.TypeItem
-import com.android.tools.metalava.model.VariableTypeItem
 import com.android.tools.metalava.model.VisibilityLevel
 import com.android.tools.metalava.model.psi.PsiItem.Companion.isKotlin
 import com.android.tools.metalava.testing.KnownSourceFiles.jetbrainsNullableTypeUseSource
@@ -116,8 +115,7 @@
             val variableMethod = methods[2]
             val variable = variableMethod.returnType()
             val typeParameter = variableMethod.typeParameterList().typeParameters().single()
-            assertThat(variable).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((variable as VariableTypeItem).asTypeParameter).isEqualTo(typeParameter)
+            variable.assertReferencesTypeParameter(typeParameter)
             assertThat(variable.annotationNames()).containsExactly("test.pkg.A")
             assertThat(variableMethod.annotationNames()).isEmpty()
         }
diff --git a/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiParameterItemTest.kt b/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiParameterItemTest.kt
index a727852..3b7eb5f 100644
--- a/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiParameterItemTest.kt
+++ b/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiParameterItemTest.kt
@@ -39,12 +39,10 @@
         }
     }
 
-    @Test
-    fun `actuals get params from expects`() {
-        // todo(b/301598511): use different modules for actual and expect to with k2 uast
-        testCodebase(
+    private fun `actuals get params from expects`(isK2: Boolean) {
+        val commonSource =
             kotlin(
-                "src/commonMain/Expect.kt",
+                "commonMain/src/Expect.kt",
                 """
                     expect suspend fun String.testFun(param: String = "")
                     expect class Test(param: String = "") {
@@ -55,10 +53,14 @@
                         )
                     }
                 """
-            ),
-            kotlin(
-                "src/jvmMain/Actual.kt",
-                """
+            )
+        testCodebase(
+            commonSources = listOf(commonSource),
+            sources =
+                listOf(
+                    kotlin(
+                        "jvmMain/src/Actual.kt",
+                        """
                     actual suspend fun String.testFun(param: String) {}
                     actual class Test actual constructor(param: String) {
                         actual fun something(
@@ -68,7 +70,10 @@
                         ) {}
                     }
                 """
-            )
+                    ),
+                    commonSource,
+                ),
+            isK2 = isK2
         ) { codebase ->
             // Expect classes are ignored by UAST/Kotlin light classes, verify we test actuals
             val actualFile = codebase.assertClass("ActualKt").getSourceFile()
@@ -114,4 +119,14 @@
             }
         }
     }
+
+    @Test
+    fun `actuals get params from expects -- K1`() {
+        `actuals get params from expects`(isK2 = false)
+    }
+
+    @Test
+    fun `actuals get params from expects -- K2`() {
+        `actuals get params from expects`(isK2 = true)
+    }
 }
diff --git a/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiSourceParserTest.kt b/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiSourceParserTest.kt
index e555bea..c9efe2d 100644
--- a/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiSourceParserTest.kt
+++ b/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiSourceParserTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.tools.metalava.model.psi
 
+import com.android.tools.metalava.model.source.SourceSet
 import com.android.tools.metalava.testing.java
 import kotlin.test.assertEquals
 import org.junit.Test
@@ -59,8 +60,8 @@
             // basically redoing what the previous code did to make sure that the underlying code
             // behaved exactly as expected. That means that the same error will be reported twice.
             val src = listOf(projectDir.resolve("src"))
-            val files = gatherSources(reporter, src)
-            val roots = extractRoots(reporter, files)
+            val sourceSet = SourceSet.createFromSourcePath(reporter, src)
+            val roots = sourceSet.extractRoots(reporter).sourcePath
             assertEquals(1, roots.size)
             assertEquals(src[0].path, roots[0].path)
 
diff --git a/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiTypeItemAssignabilityTest.kt b/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiTypeItemAssignabilityTest.kt
index 738b60c..a5f7916 100644
--- a/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiTypeItemAssignabilityTest.kt
+++ b/metalava-model-psi/src/test/java/com/android/tools/metalava/model/psi/PsiTypeItemAssignabilityTest.kt
@@ -77,10 +77,8 @@
             )
 
         testCodebase(sources = sourceFiles, classPath = listOf(getAndroidJar())) { codebase ->
-            val javaSubject =
-                codebase.findClass("test.foo.JavaSubject") ?: error("Cannot find java subject")
-            val kotlinSubject =
-                codebase.findClass("test.foo.KotlinSubject") ?: error("Cannot find subject")
+            val javaSubject = codebase.assertClass("test.foo.JavaSubject")
+            val kotlinSubject = codebase.assertClass("test.foo.KotlinSubject")
             val testSubjects = listOf(javaSubject, kotlinSubject)
             // helper method to check assignability between fields
             fun String.isAssignableFromWithoutUnboxing(otherField: String): Boolean {
diff --git a/metalava-model-psi/src/test/resources/model-test-suite-baseline.txt b/metalava-model-psi/src/test/resources/model-test-suite-baseline.txt
index 9e0b0c3..14eb6c1 100644
--- a/metalava-model-psi/src/test/resources/model-test-suite-baseline.txt
+++ b/metalava-model-psi/src/test/resources/model-test-suite-baseline.txt
@@ -2,12 +2,15 @@
   annotation with enum values[psi,java]
 
 com.android.tools.metalava.model.testsuite.typeitem.CommonTypeItemTest
-  check TypeItem asClass()[psi,java]
+  Test Kotlin collection removeAll parameter type[psi,kotlin]
 
 com.android.tools.metalava.model.testsuite.typeitem.CommonTypeModifiersTest
   Test inner parameterized types with annotations[psi,java]
   Test nullability of outer classes[psi,java]
 
+com.android.tools.metalava.model.testsuite.typeitem.CommonTypeParameterItemTest
+  Test type parameter with annotations[psi,kotlin]
+
 com.android.tools.metalava.model.testsuite.typeitem.CommonTypeStringTest
   Type string[psi,java,null annotated parameterized inner type - annotated]
   Type string[psi,java,null annotated parameterized inner type - kotlin nulls]
diff --git a/metalava-model-psi/src/main/java/com/android/tools/lint/checks/infrastructure/ClassName.kt b/metalava-model-source/src/main/java/com/android/tools/lint/checks/infrastructure/ClassName.kt
similarity index 97%
rename from metalava-model-psi/src/main/java/com/android/tools/lint/checks/infrastructure/ClassName.kt
rename to metalava-model-source/src/main/java/com/android/tools/lint/checks/infrastructure/ClassName.kt
index c0df7a4..db929cc 100644
--- a/metalava-model-psi/src/main/java/com/android/tools/lint/checks/infrastructure/ClassName.kt
+++ b/metalava-model-source/src/main/java/com/android/tools/lint/checks/infrastructure/ClassName.kt
@@ -16,8 +16,8 @@
 
 package com.android.tools.lint.checks.infrastructure
 
-import com.android.SdkConstants.DOT_JAVA
-import com.android.SdkConstants.DOT_KT
+import com.android.tools.metalava.model.source.utils.DOT_JAVA
+import com.android.tools.metalava.model.source.utils.DOT_KT
 import java.util.regex.Pattern
 
 // Copy in metalava from lint to avoid compilation dependency directly on lint-tests
diff --git a/metalava-model-source/src/main/java/com/android/tools/metalava/model/source/SourceParser.kt b/metalava-model-source/src/main/java/com/android/tools/metalava/model/source/SourceParser.kt
index 7ea86a3..6fc8437 100644
--- a/metalava-model-source/src/main/java/com/android/tools/metalava/model/source/SourceParser.kt
+++ b/metalava-model-source/src/main/java/com/android/tools/metalava/model/source/SourceParser.kt
@@ -33,17 +33,23 @@
     /**
      * Parse a set of sources into a [SourceCodebase].
      *
-     * @param sources the list of source files.
+     * @param sourceSet the list of source files and root directories.
+     * @param commonSourceSet the list of source files and root directories in the common module.
      * @param description the description to use for [Codebase.description].
-     * @param sourcePath a possibly empty list of root directories within which sources files may be
-     *   found.
      * @param classPath the possibly empty list of jar files which may provide additional classes
      *   referenced by the sources.
+     *
+     * "Common module" is the term used in Kotlin multi-platform projects where platform-agnostic
+     * business logic and `expect` declarations are declared. (Counterparts, like platform-specific
+     * logic and `actual` declarations are declared at platform-specific modules, of course.) To
+     * that end, [commonSourceSet] will be used for Kotlin multi-platform projects only. All others,
+     * such as Java only or non-KMP Kotlin projects, won't need to set it, i.e., should pass source
+     * files and root directories via [sourceSet], not [commonSourceSet].
      */
     fun parseSources(
-        sources: List<File>,
+        sourceSet: SourceSet,
+        commonSourceSet: SourceSet,
         description: String,
-        sourcePath: List<File>,
         classPath: List<File>,
     ): SourceCodebase
 
diff --git a/metalava-model-source/src/main/java/com/android/tools/metalava/model/source/SourceSet.kt b/metalava-model-source/src/main/java/com/android/tools/metalava/model/source/SourceSet.kt
new file mode 100644
index 0000000..ac835e5
--- /dev/null
+++ b/metalava-model-source/src/main/java/com/android/tools/metalava/model/source/SourceSet.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.model.source
+
+import com.android.tools.metalava.model.source.utils.DOT_JAVA
+import com.android.tools.metalava.model.source.utils.DOT_KT
+import com.android.tools.metalava.model.source.utils.OVERVIEW_HTML
+import com.android.tools.metalava.model.source.utils.PACKAGE_HTML
+import com.android.tools.metalava.model.source.utils.findPackage
+import com.android.tools.metalava.reporter.Issues
+import com.android.tools.metalava.reporter.Reporter
+import java.io.File
+import java.nio.file.Files
+
+/**
+ * An abstraction of source files and root directories.
+ *
+ * Those are always paired together or computed from one another.
+ *
+ * @param sources the list of source files
+ * @param sourcePath a possibly empty list of root directories within which source files may be
+ *   found.
+ */
+class SourceSet(val sources: List<File>, val sourcePath: List<File>) {
+
+    val absoluteSources: List<File>
+        get() {
+            return sources.map { it.absoluteFile }
+        }
+
+    val absoluteSourcePaths: List<File>
+        get() {
+            return sourcePath.filter { it.path.isNotBlank() }.map { it.absoluteFile }
+        }
+
+    /** Creates a copy of [SourceSet], but with elements mapped with [File.getAbsoluteFile] */
+    fun absoluteCopy(): SourceSet {
+        return SourceSet(absoluteSources, absoluteSourcePaths)
+    }
+
+    /**
+     * Creates a new instance of [SourceSet], adding in source roots implied by the source files in
+     * the current [SourceSet]
+     */
+    fun extractRoots(reporter: Reporter): SourceSet {
+        val sourceRoots = extractRoots(reporter, sources, sourcePath.toMutableList())
+        return SourceSet(sources, sourceRoots)
+    }
+
+    companion object {
+        fun empty(): SourceSet = SourceSet(emptyList(), emptyList())
+
+        /** * Creates [SourceSet] from the given [sourcePath] */
+        fun createFromSourcePath(
+            reporter: Reporter,
+            sourcePath: List<File>,
+            fileTester: (File) -> Boolean = ::isSupportedSource,
+        ): SourceSet {
+            val sources = gatherSources(reporter, sourcePath, fileTester)
+            return SourceSet(sources, sourcePath)
+        }
+
+        private fun skippableDirectory(file: File): Boolean =
+            file.path.endsWith(".git") && file.name == ".git"
+
+        private fun isSupportedSource(file: File): Boolean =
+            file.name.endsWith(DOT_JAVA) ||
+                file.name.endsWith(DOT_KT) ||
+                file.name.equals(PACKAGE_HTML) ||
+                file.name.equals(OVERVIEW_HTML)
+
+        private fun addSourceFiles(
+            reporter: Reporter,
+            list: MutableList<File>,
+            file: File,
+            fileTester: (File) -> Boolean = ::isSupportedSource,
+        ) {
+            if (file.isDirectory) {
+                if (skippableDirectory(file)) {
+                    return
+                }
+                if (Files.isSymbolicLink(file.toPath())) {
+                    reporter.report(
+                        Issues.IGNORING_SYMLINK,
+                        file,
+                        "Ignoring symlink during source file discovery directory traversal"
+                    )
+                    return
+                }
+                val files = file.listFiles()
+                if (files != null) {
+                    for (child in files) {
+                        addSourceFiles(reporter, list, child)
+                    }
+                }
+            } else if (file.isFile) {
+                if (fileTester.invoke(file)) {
+                    list.add(file)
+                }
+            }
+        }
+
+        private fun gatherSources(
+            reporter: Reporter,
+            sourcePath: List<File>,
+            fileTester: (File) -> Boolean = ::isSupportedSource,
+        ): List<File> {
+            val sources = mutableListOf<File>()
+            for (file in sourcePath) {
+                if (file.path.isBlank()) {
+                    // --source-path "" means don't search source path; use "." for pwd
+                    continue
+                }
+                addSourceFiles(reporter, sources, file.absoluteFile, fileTester)
+            }
+            return sources.sortedWith(compareBy { it.name })
+        }
+
+        private fun extractRoots(
+            reporter: Reporter,
+            sources: List<File>,
+            sourceRoots: MutableList<File> = mutableListOf()
+        ): List<File> {
+            // Cache for each directory since computing root for a source file is expensive
+            val dirToRootCache = mutableMapOf<String, File>()
+            for (file in sources) {
+                val parent = file.parentFile ?: continue
+                val found = dirToRootCache[parent.path]
+                if (found != null) {
+                    continue
+                }
+
+                val root = findRoot(reporter, file) ?: continue
+                dirToRootCache[parent.path] = root
+
+                if (!sourceRoots.contains(root)) {
+                    sourceRoots.add(root)
+                }
+            }
+            return sourceRoots
+        }
+
+        /**
+         * If given a full path to a Java or Kotlin source file, produces the path to the source
+         * root if possible.
+         */
+        private fun findRoot(reporter: Reporter, file: File): File? {
+            val path = file.path
+            if (path.endsWith(DOT_JAVA) || path.endsWith(DOT_KT)) {
+                val pkg = findPackage(file) ?: return null
+                val parent = file.parentFile ?: return null
+                val endIndex = parent.path.length - pkg.length
+                val before = path[endIndex - 1]
+                if (before == '/' || before == '\\') {
+                    return File(path.substring(0, endIndex))
+                } else {
+                    reporter.report(
+                        Issues.IO_ERROR,
+                        file,
+                        "Unable to determine the package name. " +
+                            "This usually means that a source file was where the directory does not seem to match the package " +
+                            "declaration; we expected the path $path to end with /${pkg.replace('.', '/') + '/' + file.name}"
+                    )
+                }
+            }
+            return null
+        }
+    }
+}
diff --git a/metalava-model-source/src/main/java/com/android/tools/metalava/model/source/utils/SourceSetUtils.kt b/metalava-model-source/src/main/java/com/android/tools/metalava/model/source/utils/SourceSetUtils.kt
new file mode 100644
index 0000000..84182ca
--- /dev/null
+++ b/metalava-model-source/src/main/java/com/android/tools/metalava/model/source/utils/SourceSetUtils.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.model.source.utils
+
+import com.android.tools.lint.checks.infrastructure.ClassName
+import java.io.File
+
+const val PACKAGE_HTML = "package.html"
+const val OVERVIEW_HTML = "overview.html"
+const val DOT_JAVA = ".java"
+const val DOT_KT = ".kt"
+
+/** Finds the package of the given Java/Kotlin source file, if possible */
+fun findPackage(file: File): String? {
+    val source = file.readText(Charsets.UTF_8)
+    return findPackage(source)
+}
+
+/** Finds the package of the given Java/Kotlin source code, if possible */
+private fun findPackage(source: String): String? {
+    return ClassName(source).packageName
+}
diff --git a/metalava-model-source/src/testFixtures/java/com/android/tools/metalava/model/source/SourceModelSuiteRunner.kt b/metalava-model-source/src/testFixtures/java/com/android/tools/metalava/model/source/SourceModelSuiteRunner.kt
index 7eba4aa..76765e3 100644
--- a/metalava-model-source/src/testFixtures/java/com/android/tools/metalava/model/source/SourceModelSuiteRunner.kt
+++ b/metalava-model-source/src/testFixtures/java/com/android/tools/metalava/model/source/SourceModelSuiteRunner.kt
@@ -70,9 +70,9 @@
         return environmentManager
             .createSourceParser(reporter, noOpAnnotationManager)
             .parseSources(
-                sources = sources.map { it.createFile(directory) },
+                SourceSet(sources.map { it.createFile(directory) }, listOf(directory)),
+                SourceSet.empty(),
                 description = "Test Codebase",
-                sourcePath = listOf(directory),
                 classPath = classPath,
             )
     }
diff --git a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/BaseModelTest.kt b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/BaseModelTest.kt
index 09211dc..0e06271 100644
--- a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/BaseModelTest.kt
+++ b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/BaseModelTest.kt
@@ -43,8 +43,12 @@
  * ran last. However, the test reports in the model implementation projects do list each run
  * separately. If this is an issue then the [ModelSuiteRunner] implementations could all be moved
  * into the same project and run tests against them all at the same time.
+ *
+ * @param fixedParameters A set of fixed [TestParameters], used for creating tests that run for a
+ *   fixed set of [ModelSuiteRunner] and [InputFormat]. This is useful when writing model specific
+ *   tests that want to take advantage of the infrastructure for running suite tests.
  */
-abstract class BaseModelTest : Assertions {
+abstract class BaseModelTest(fixedParameters: TestParameters? = null) : Assertions {
 
     /**
      * Set by injection by [Parameterized] after class initializers are called.
@@ -69,6 +73,12 @@
      */
     @Parameter(0) lateinit var baseParameters: TestParameters
 
+    init {
+        if (fixedParameters != null) {
+            this.baseParameters = fixedParameters
+        }
+    }
+
     /** The [ModelSuiteRunner] that this test must use. */
     private val runner by lazy { baseParameters.runner }
 
@@ -131,40 +141,35 @@
         val testFiles: List<TestFile>,
     )
 
+    /** Create an [InputSet] from a list of [TestFile]s. */
+    fun inputSet(testFiles: List<TestFile>): InputSet = inputSet(*testFiles.toTypedArray())
+
     /**
      * Create an [InputSet].
      *
-     * It is an error if [testFiles] is empty or if [testFiles] have different [InputFormat]. That
-     * means that it is not currently possible to mix Kotlin and Java files.
+     * It is an error if [testFiles] is empty or if [testFiles] have a mixture of source
+     * ([InputFormat.JAVA] or [InputFormat.KOTLIN]) and signature ([InputFormat.SIGNATURE]). If it
+     * contains both [InputFormat.JAVA] and [InputFormat.KOTLIN] then the latter will be used.
      */
     fun inputSet(vararg testFiles: TestFile): InputSet {
         if (testFiles.isEmpty()) {
             throw IllegalStateException("Must provide at least one source file")
         }
 
-        val (htmlFiles, nonHtmlFiles) =
-            testFiles.partition { it.targetRelativePath.endsWith(".html") }
+        val inputFormat =
+            testFiles
+                .asSequence()
+                // Map to path.
+                .map { it.targetRelativePath }
+                // Ignore HTML files.
+                .filter { !it.endsWith(".html") }
+                // Map to InputFormat.
+                .map { InputFormat.fromFilename(it) }
+                // Combine InputFormats to produce a single one, may throw an exception if they
+                // are incompatible.
+                .reduce { if1, if2 -> if1.combineWith(if2) }
 
-        // Make sure that all the test files are the same InputFormat. Ignore HTML files.
-        val byInputFormat = nonHtmlFiles.groupBy { InputFormat.fromFilename(it.targetRelativePath) }
-
-        val inputFormatCount = byInputFormat.size
-        if (inputFormatCount != 1) {
-            throw IllegalStateException(
-                buildString {
-                    append(
-                        "All files in the list must be the same input format, but found $inputFormatCount different input formats:\n"
-                    )
-                    byInputFormat.forEach { (format, files) ->
-                        append("    $format\n")
-                        files.forEach { append("        $it\n") }
-                    }
-                }
-            )
-        }
-
-        val (inputFormat, files) = byInputFormat.entries.single()
-        return InputSet(inputFormat, files + htmlFiles)
+        return InputSet(inputFormat, testFiles.toList())
     }
 
     /**
@@ -270,10 +275,15 @@
         }
     }
 
-    /** Create a signature [TestFile] with the supplied [contents]. */
-    fun signature(contents: String): TestFile {
-        return TestFiles.source("api.txt", contents.trimIndent())
-    }
+    /**
+     * Create a signature [TestFile] with the supplied [contents] in a file with a path of
+     * `api.txt`.
+     */
+    fun signature(contents: String): TestFile = signature("api.txt", contents)
+
+    /** Create a signature [TestFile] with the supplied [contents] in a file with a path of [to]. */
+    fun signature(to: String, contents: String): TestFile =
+        TestFiles.source(to, contents.trimIndent())
 }
 
 private const val GRADLEW_UPDATE_MODEL_TEST_SUITE_BASELINE =
@@ -296,19 +306,15 @@
                     // Run the test even if it is expected to fail as a change that fixes one test
                     // may fix more. Instead, this will just discard any failure.
                     base.evaluate()
-                    if (expectedFailure) {
-                        // If a test that was expected to fail passes then updating the baseline
-                        // will remove that test from the expected test failures.
-                        System.err.println(
-                            "Test was expected to fail but passed, please run $GRADLEW_UPDATE_MODEL_TEST_SUITE_BASELINE"
-                        )
-                    }
                 } catch (e: Throwable) {
                     if (expectedFailure) {
                         // If this was expected to fail then throw an AssumptionViolatedException
-                        // so it is not treated as either a pass or fail.
+                        // that way it is not treated as either a pass or fail. Indent the exception
+                        // output and include it in the message instead of chaining the exception as
+                        // that reads better than the default formatting of chained exceptions.
+                        val actualErrorStackTrace = e.stackTraceToString().prependIndent("    ")
                         throw AssumptionViolatedException(
-                            "Test skipped since it is listed in the baseline file for $runner"
+                            "Test skipped since it is listed in the baseline file for $runner.\n$actualErrorStackTrace"
                         )
                     } else {
                         // Inform the developer on how to ignore this failing test.
@@ -320,6 +326,24 @@
                         throw e
                     }
                 }
+
+                // Perform this check outside the try...catch block otherwise the exception gets
+                // caught, making it look like an actual failing test.
+                if (expectedFailure) {
+                    // If a test that was expected to fail passes then updating the baseline
+                    // will remove that test from the expected test failures. Fail the test so
+                    // that the developer will be forced to clean it up.
+                    throw IllegalStateException(
+                        """
+                            **************************************************************************************************
+                                Test was listed in the baseline file as it was expected to fail but it passed, please run:
+                                    $GRADLEW_UPDATE_MODEL_TEST_SUITE_BASELINE
+                            **************************************************************************************************
+
+                        """
+                            .trimIndent()
+                    )
+                }
             }
         }
     }
diff --git a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/BootstrapSourceModelProviderTest.kt b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/BootstrapSourceModelProviderTest.kt
index a16826f..d2fb255 100644
--- a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/BootstrapSourceModelProviderTest.kt
+++ b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/BootstrapSourceModelProviderTest.kt
@@ -18,7 +18,9 @@
 
 import com.android.tools.metalava.model.AnnotationRetention
 import com.android.tools.metalava.model.ClassTypeItem
+import com.android.tools.metalava.model.DefaultAnnotationSingleAttributeValue
 import com.android.tools.metalava.model.PrimitiveTypeItem
+import com.android.tools.metalava.model.ThrowableType
 import com.android.tools.metalava.model.VariableTypeItem
 import com.android.tools.metalava.testing.java
 import com.google.common.truth.Truth.assertThat
@@ -362,7 +364,6 @@
             assertEquals(2, classItem.interfaceTypes().count())
 
             assertNotNull(superClassType)
-            assertEquals(true, superClassType is ClassTypeItem)
             assertEquals(null, superClassType.asClass())
         }
     }
@@ -439,6 +440,8 @@
             val custAnno1Attr3 = customAnno1.findAttribute("cls")
             val annoClassItem1 = codebase.assertClass("test.anno.FieldInfo")
             val retAnno = annoClassItem1.assertAnnotation("java.lang.annotation.Retention")
+            val tarAnno = annoClassItem1.assertAnnotation("java.lang.annotation.Target")
+            val tarAnnoAtrr1 = tarAnno.findAttribute("value")
 
             val customAnno2 = fieldItem.assertAnnotation("anno.FieldValue")
             val annoClassItem2 = codebase.assertClass("anno.FieldValue")
@@ -448,6 +451,7 @@
 
             assertEquals(true, nullAnno.isNullable())
 
+            assertEquals(3, customAnno1.attributes.count())
             assertEquals(false, customAnno1.isRetention())
             assertNotNull(custAnno1Attr1)
             assertNotNull(custAnno1Attr2)
@@ -466,6 +470,18 @@
             assertEquals(annoClassItem2, customAnno2.resolve())
             assertNotNull(custAnno2Attr1)
             assertEquals(12, custAnno2Attr1.value.value())
+
+            assertEquals("@test.Nullable", nullAnno.toSource())
+
+            assertEquals(
+                "@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)",
+                retAnno.toSource()
+            )
+            assertEquals(
+                "@java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD)",
+                tarAnno.toSource()
+            )
+            assertEquals(true, tarAnnoAtrr1!!.value is DefaultAnnotationSingleAttributeValue)
         }
     }
 
@@ -625,47 +641,33 @@
             val testClass = codebase.assertClass("test.pkg.Test")
             val testClass1 = codebase.assertClass("test.pkg.Test1")
             val testClass2 = codebase.assertClass("test.pkg.Test1.Test2")
-            val testClassType = testClass.toType()
-            val testClassType1 = testClass1.toType()
-            val testClassType2 = testClass2.toType()
+            val testClassType = testClass.type()
+            val testClassType1 = testClass1.type()
+            val testClassType2 = testClass2.type()
 
             assertThat(testClassType).isInstanceOf(ClassTypeItem::class.java)
-            testClassType as ClassTypeItem
             assertEquals("test.pkg.Test", testClassType.qualifiedName)
-            assertEquals(0, testClassType.parameters.count())
+            assertEquals(0, testClassType.arguments.count())
 
             assertThat(testClassType1).isInstanceOf(ClassTypeItem::class.java)
-            testClassType1 as ClassTypeItem
             assertEquals("test.pkg.Test1", testClassType1.qualifiedName)
-            assertEquals(1, testClassType1.parameters.count())
-            val paramItem1 = testClassType1.parameters.single()
-            assertThat(paramItem1).isInstanceOf(VariableTypeItem::class.java)
-            paramItem1 as VariableTypeItem
-            assertEquals("S", paramItem1.toString())
-            assertEquals(
-                testClass1.typeParameterList().typeParameters().single(),
-                paramItem1.asTypeParameter
-            )
-            assertEquals(0, paramItem1.asTypeParameter.typeBounds().count())
+            assertEquals(1, testClassType1.arguments.count())
+            val typeArgument1 = testClassType1.arguments.single()
+            val typeParameter1 = testClass1.typeParameterList().typeParameters().single()
+            typeArgument1.assertReferencesTypeParameter(typeParameter1)
+            assertEquals("S", (typeArgument1 as VariableTypeItem).toString())
+            assertEquals(0, typeParameter1.typeBounds().count())
             assertEquals("test.pkg.Test1<S>", testClassType1.toString())
             assertEquals(null, testClassType1.outerClassType)
 
             assertThat(testClassType2).isInstanceOf(ClassTypeItem::class.java)
-            testClassType2 as ClassTypeItem
             assertEquals("test.pkg.Test1.Test2", testClassType2.qualifiedName)
-            assertEquals(1, testClassType2.parameters.count())
-            val paramItem2 = testClassType2.parameters.single()
-            assertThat(paramItem2).isInstanceOf(VariableTypeItem::class.java)
-            paramItem2 as VariableTypeItem
-            assertEquals("T", paramItem2.toString())
-            assertEquals(
-                testClass2.typeParameterList().typeParameters().single(),
-                paramItem2.asTypeParameter
-            )
-            assertEquals(
-                "test.pkg.Test",
-                paramItem2.asTypeParameter.typeBounds().single().toString()
-            )
+            assertEquals(1, testClassType2.arguments.count())
+            val typeArgument2 = testClassType2.arguments.single()
+            val typeParameter2 = testClass2.typeParameterList().typeParameters().single()
+            typeArgument2.assertReferencesTypeParameter(typeParameter2)
+            assertEquals("T", (typeArgument2 as VariableTypeItem).toString())
+            assertEquals("test.pkg.Test", typeParameter2.typeBounds().single().toString())
             assertEquals("test.pkg.Test1<S>.Test2<T>", testClassType2.toString())
             assertEquals(testClassType1, testClassType2.outerClassType)
         }
@@ -694,14 +696,14 @@
             assertEquals(2, testClass.constructors().count())
             val constructorItem = testClass.constructors().first()
             assertEquals("Test", constructorItem.name())
-            assertEquals(testClass.toType(), constructorItem.returnType())
+            assertEquals(testClass.type(), constructorItem.returnType())
             assertEquals(false, testClass.hasImplicitDefaultConstructor())
 
             val testClass1 = codebase.assertClass("test.pkg.Test.Test1")
             val constructorItem1 = testClass1.constructors().single()
             assertEquals("Test1", constructorItem1.name())
             assertEquals("test.pkg.Test.Test1", constructorItem1.returnType().toString())
-            assertEquals(testClass1.toType(), constructorItem1.returnType())
+            assertEquals(testClass1.type(), constructorItem1.returnType())
             assertEquals(true, testClass1.hasImplicitDefaultConstructor())
         }
     }
@@ -745,19 +747,16 @@
 
             assertEquals(
                 classParameterNames,
-                classTypeParameterList.typeParameters().map { it.simpleName() }
+                classTypeParameterList.typeParameters().map { it.name() }
             )
-            assertEquals(
-                emptyList(),
-                annoTypeParameterList.typeParameters().map { it.simpleName() }
-            )
+            assertEquals(emptyList(), annoTypeParameterList.typeParameters().map { it.name() })
             assertEquals(
                 method1ParameterNames,
-                method1TypeParameterList.typeParameters().map { it.simpleName() }
+                method1TypeParameterList.typeParameters().map { it.name() }
             )
             assertEquals(
                 method2TypeParameterNames,
-                method2TypeParameterList.typeParameters().map { it.simpleName() }
+                method2TypeParameterList.typeParameters().map { it.name() }
             )
 
             assertEquals(
@@ -800,7 +799,10 @@
             val ioExceptionClass = codebase.assertClass("java.io.IOException")
             val methodItem = testClass.assertMethod("foo", "")
 
-            assertEquals(listOf(ioExceptionClass, testExceptionClass), methodItem.throwsTypes())
+            assertEquals(
+                listOf(ioExceptionClass, testExceptionClass).map(ThrowableType::ofClass),
+                methodItem.throwsTypes()
+            )
         }
     }
 
@@ -847,7 +849,7 @@
 
             assertEquals("Test", ctorItem.name())
             assertEquals(classItem, ctorItem.containingClass())
-            assertEquals(classItem.toType(), ctorItem.returnType())
+            assertEquals(classItem.type(), ctorItem.returnType())
             assertEquals(
                 ctorItem.modifiers.getVisibilityLevel(),
                 classItem.modifiers.getVisibilityLevel()
@@ -865,18 +867,19 @@
                     package test.pkg;
 
                     import java.lang.annotation.ElementType;
+                    import java.lang.annotation.Target;
 
                     public class Test {
                         public void foo(@ParameterName("TestParam") @DefaultValue(5) int parameter) {
                         }
                     }
 
-                    @Target(value={ElementType.PARAMETER})
+                    @Target(ElementType.PARAMETER)
                     @interface DefaultValue {
                         int value();
                     }
 
-                    @Target(value={ElementType.PARAMETER})
+                    @Target(ElementType.PARAMETER)
                     @interface ParameterName {
                         String value();
                     }
diff --git a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/CommonModelTest.kt b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/CommonModelTest.kt
index 3349bc1..d46deff 100644
--- a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/CommonModelTest.kt
+++ b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/CommonModelTest.kt
@@ -65,7 +65,7 @@
                     """
                         package test.pkg;
                         public class Foo {
-                            public void foo(int i) {}                        
+                            public void foo(int i) {}
                         }
                     """
                 ),
@@ -73,8 +73,8 @@
                     """
                         package test.pkg;
                         public class Bar extends Foo {
-                            public void foo(int i) {}                        
-                            public int bar(String s) {return s.length();}                        
+                            public void foo(int i) {}
+                            public int bar(String s) {return s.length();}
                         }
                     """
                 ),
@@ -91,4 +91,95 @@
             )
         }
     }
+
+    @Test
+    fun `Test iterate and resolve unknown super classes`() {
+        // TODO(b/323516595): Find a better way.
+        runCodebaseTest(
+            inputSet(
+                signature(
+                    """
+                        // Signature format: 2.0
+                        package test.pkg {
+                          public class Foo extends test.pkg.Unknown {
+                            ctor public Foo();
+                          }
+                          public class Bar extends test.unknown.Foo {
+                            ctor public Bar();
+                          }
+                        }
+                    """
+                ),
+            ),
+            inputSet(
+                java(
+                    """
+                        package test.pkg;
+                        public class Foo extends test.pkg.Unknown {
+                        }
+                    """
+                ),
+                java(
+                    """
+                        package test.pkg;
+                        public class Bar extends test.unknown.Foo {
+                        }
+                    """
+                ),
+            ),
+        ) {
+            // Iterate over the codebase and try and find every item that is visited.
+            for (classItem in codebase.getPackages().allClasses()) {
+                // Resolve the super class which might trigger a change in the packages/classes.
+                classItem.superClass()
+            }
+        }
+    }
+
+    @Test
+    fun `Test iterate and resolve unknown interface classes`() {
+        // TODO(b/323516595): Find a better way.
+        runCodebaseTest(
+            inputSet(
+                signature(
+                    """
+                        // Signature format: 2.0
+                        package test.pkg {
+                          public class Foo implements test.pkg.Unknown {
+                            ctor public Foo();
+                          }
+                          public class Bar implements test.unknown.Foo {
+                            ctor public Bar();
+                          }
+                        }
+                    """
+                ),
+            ),
+            inputSet(
+                java(
+                    """
+                        package test.pkg;
+                        public class Foo implements test.pkg.Unknown {
+                        }
+                    """
+                ),
+                java(
+                    """
+                        package test.pkg;
+                        public class Bar implements test.unknown.Foo {
+                        }
+                    """
+                ),
+            ),
+        ) {
+            // Iterate over the codebase and try and find every item that is visited.
+            for (classItem in codebase.getPackages().allClasses()) {
+                for (interfaceType in classItem.interfaceTypes()) {
+                    // Resolve the interface type which might trigger a change in the
+                    // packages/classes.
+                    interfaceType.asClass()
+                }
+            }
+        }
+    }
 }
diff --git a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/InputFormat.kt b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/InputFormat.kt
index d4209b5..33eb57ec 100644
--- a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/InputFormat.kt
+++ b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/InputFormat.kt
@@ -53,6 +53,14 @@
         SourceLanguage.KOTLIN,
     );
 
+    fun combineWith(other: InputFormat): InputFormat {
+        if (this == other) return this
+        if (this == SIGNATURE || other == SIGNATURE) error("Cannot mix signature and source files")
+        // When mixing Kotlin and Java then it should be treated as Kotlin as a Kotlin provider can
+        // handle Java but the reverse is not true.
+        return KOTLIN
+    }
+
     companion object {
         fun fromFilename(path: String): InputFormat {
             val extension = File(path).extension
diff --git a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/annotationitem/CommonAnnotationItemTest.kt b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/annotationitem/CommonAnnotationItemTest.kt
index 2ab0312..f1e3cfb 100644
--- a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/annotationitem/CommonAnnotationItemTest.kt
+++ b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/annotationitem/CommonAnnotationItemTest.kt
@@ -571,6 +571,49 @@
     }
 
     @Test
+    fun `annotation with constant literal values`() {
+        runCodebaseTest(
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                      @test.pkg.Test.Anno(test.pkg.Test.FIELD)
+                      public class Test {
+                        ctor public Test();
+                        field public static final int FIELD = 5;
+                      }
+
+                      public @interface Test.Anno {
+                         method public Int value();
+                      }
+                    }
+                """
+            ),
+            java(
+                """
+                    package test.pkg;
+
+                    @Test.Anno(Test.FIELD)
+                    public class Test {
+                        public Test() {}
+
+                        public static final int FIELD = 5;
+
+                        public @interface Anno {
+                          int value();
+                        }
+                    }
+                """
+            ),
+        ) {
+            val testClass = codebase.assertClass("test.pkg.Test")
+            val anno = testClass.modifiers.annotations().single()
+
+            anno.assertAttributeValue("value", 5)
+        }
+    }
+
+    @Test
     fun `annotation toSource() with annotation values`() {
         runCodebaseTest(
             signature(
@@ -682,7 +725,7 @@
                     package test.pkg {
                       @test.pkg.Test.Anno(
                           charValue = 'a',
-                          charArrayValue = {'a', 'b'},
+                          charArrayValue = {'a', '\uFF00'},
                       )
                       public class Test {
                         ctor public Test();
@@ -701,7 +744,7 @@
 
                     @Test.Anno(
                       charValue = 'a',
-                      charArrayValue = {'a', 'b'}
+                      charArrayValue = {'a', '\uFF00'}
                     )
                     public class Test {
                         public Test() {}
@@ -717,7 +760,7 @@
             val testClass = codebase.assertClass("test.pkg.Test")
             val anno = testClass.modifiers.annotations().single()
 
-            val toSource = "@test.pkg.Test.Anno(charValue='a', charArrayValue={'a', 'b'})"
+            val toSource = "@test.pkg.Test.Anno(charValue='a', charArrayValue={'a', '\\uff00'})"
             assertEquals(toSource, anno.toSource())
         }
     }
@@ -1075,6 +1118,50 @@
     }
 
     @Test
+    fun `annotation toSource() with constant literal values`() {
+        runCodebaseTest(
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                      @test.pkg.Test.Anno(test.pkg.Test.FIELD)
+                      public class Test {
+                        ctor public Test();
+                        field public static final int FIELD = 5;
+                      }
+
+                      public @interface Test.Anno {
+                         method public Int value();
+                      }
+                    }
+                """
+            ),
+            java(
+                """
+                    package test.pkg;
+
+                    @Test.Anno(Test.FIELD)
+                    public class Test {
+                        public Test() {}
+
+                        public static final int FIELD = 5;
+
+                        public @interface Anno {
+                          int value();
+                        }
+                    }
+                """
+            ),
+        ) {
+            val testClass = codebase.assertClass("test.pkg.Test")
+            val anno = testClass.modifiers.annotations().single()
+
+            val toSource = "@test.pkg.Test.Anno(test.pkg.Test.FIELD)"
+            assertEquals(toSource, anno.toSource())
+        }
+    }
+
+    @Test
     fun `annotation toSource() with compound expression values`() {
         runCodebaseTest(
             signature(
@@ -1122,19 +1209,29 @@
     }
 
     @Test
-    fun `annotation with negative values`() {
+    fun `annotation with negative number values`() {
         runCodebaseTest(
             signature(
                 """
                     // Signature format: 2.0
                     package test.pkg {
-                      @test.pkg.Test.Anno(-1)
+                      @test.pkg.Test.Anno(
+                          doubleValue = -1.5,
+                          floatValue = -0.5F,
+                          intValue = -1,
+                          longValue = -2,
+                          shortValue = -3,
+                      )
                       public class Test {
                         ctor public Test();
                       }
 
                       public @interface Test.Anno {
-                          method public int value();
+                          method public double doubleValue();
+                          method public float floatValue();
+                          method public int intValue();
+                          method public long longValue();
+                          method public short shortValue();
                       }
                     }
                 """
@@ -1143,12 +1240,22 @@
                 """
                     package test.pkg;
 
-                    @Test.Anno(-1)
+                    @Test.Anno(
+                      doubleValue = -1.5,
+                      floatValue = -0.5F,
+                      intValue = -1,
+                      longValue = -2L,
+                      shortValue = -3,
+                    )
                     public class Test {
                         public Test() {}
 
                         public @interface Anno {
-                          int value();
+                          double doubleValue();
+                          float floatValue();
+                          int intValue();
+                          long longValue();
+                          short shortValue();
                         }
                     }
                 """
@@ -1157,8 +1264,15 @@
             val testClass = codebase.assertClass("test.pkg.Test")
             val anno = testClass.modifiers.annotations().single()
 
-            anno.assertAttributeValue("value", -1)
-            assertEquals("@test.pkg.Test.Anno(0xffffffff)", anno.toSource())
+            anno.assertAttributeValue("doubleValue", -1.5)
+            anno.assertAttributeValue("floatValue", -0.5F)
+            anno.assertAttributeValue("intValue", -1)
+            anno.assertAttributeValue("longValue", -2L)
+            anno.assertAttributeValue("shortValue", -3.toShort())
+
+            val toSource =
+                "@test.pkg.Test.Anno(doubleValue=-1.5, floatValue=-0.5F, intValue=0xffffffff, longValue=-2L, shortValue=0xfffffffd)"
+            assertEquals(toSource, anno.toSource())
         }
     }
 
diff --git a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/classitem/CommonClassItemTest.kt b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/classitem/CommonClassItemTest.kt
index 47c77cb..f2355c3 100644
--- a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/classitem/CommonClassItemTest.kt
+++ b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/classitem/CommonClassItemTest.kt
@@ -18,6 +18,7 @@
 
 import com.android.tools.metalava.model.ClassItem
 import com.android.tools.metalava.model.ClassTypeItem
+import com.android.tools.metalava.model.TypeParameterItem
 import com.android.tools.metalava.model.VariableTypeItem
 import com.android.tools.metalava.model.testsuite.BaseModelTest
 import com.android.tools.metalava.testing.java
@@ -110,6 +111,188 @@
     }
 
     @Test
+    fun `Test access type parameter of outer class in type parameters`() {
+        runCodebaseTest(
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                      public class Outer<O> {
+                      }
+                      public class Outer.Middle {
+                      }
+                      public class Outer.Middle.Inner<T extends O> {
+                      }
+                    }
+                """
+            ),
+            java(
+                """
+                    package test.pkg;
+
+                    public class Outer<O> {
+                        private Outer() {}
+                        public class Middle {
+                            private Middle() {}
+                            public class Inner<T extends O> {
+                                private Inner() {}
+                            }
+                        }
+                    }
+                """
+            ),
+            kotlin(
+                """
+                    package test.pkg
+
+                    class Outer<O> private constructor() {
+                        inner class Middle private constructor() {
+                            inner class Inner<T: O> private constructor()
+                        }
+                    }
+                """
+            ),
+        ) {
+            val oTypeParameter =
+                codebase.assertClass("test.pkg.Outer").typeParameterList().typeParameters().single()
+            val extendsType =
+                codebase
+                    .assertClass("test.pkg.Outer.Middle.Inner")
+                    .typeParameterList()
+                    .typeParameters()
+                    .first()
+                    .typeBounds()
+                    .first()
+
+            extendsType.assertReferencesTypeParameter(oTypeParameter)
+        }
+    }
+
+    @Test
+    fun `Test access type parameter of outer class in extends type`() {
+        runCodebaseTest(
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                      public class Outer<O> {
+                      }
+                      public class Outer.Middle {
+                      }
+                      public abstract class Outer.Middle.Inner extends test.pkg.Outer.GenericClass<O> {
+                      }
+                      public abstract static class Outer.GenericClass<T> {
+                        method public abstract T method();
+                      }
+                    }
+                """
+            ),
+            java(
+                """
+                    package test.pkg;
+
+                    public class Outer<O> {
+                        private Outer() {}
+                        public static abstract class GenericClass<T> {
+                            private GenericClass() {}
+                            public abstract T method();
+                        }
+                        public class Middle {
+                            private Middle() {}
+                            public abstract class Inner extends GenericClass<O> {
+                                private Inner() {}
+                            }
+                        }
+                    }
+                """
+            ),
+            kotlin(
+                """
+                    package test.pkg
+
+                    class Outer<O> private constructor() {
+                        abstract class GenericClass<T> private constructor() {
+                            abstract fun method(): T
+                        }
+                        inner class Middle private constructor() {
+                            abstract inner class Inner(o: O): GenericClass<O>()
+                        }
+                    }
+                """
+            ),
+        ) {
+            val oTypeParameter =
+                codebase.assertClass("test.pkg.Outer").typeParameterList().typeParameters().single()
+            val extendsType = codebase.assertClass("test.pkg.Outer.Middle.Inner").superClassType()!!
+            val typeArgument = extendsType.arguments.single()
+
+            typeArgument.assertReferencesTypeParameter(oTypeParameter)
+        }
+    }
+
+    @Test
+    fun `Test access type parameter of outer class in interface type`() {
+        runCodebaseTest(
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                      public class Outer<O> {
+                      }
+                      public class Outer.Middle {
+                      }
+                      public abstract class Outer.Middle.Inner implements test.pkg.Outer.GenericInterface<O> {
+                      }
+                      public interface Outer.GenericInterface<T> {
+                        method public abstract T method();
+                      }
+                    }
+                """
+            ),
+            java(
+                """
+                    package test.pkg;
+
+                    public class Outer<O> {
+                        private Outer() {}
+                        public interface GenericInterface<T> {
+                            T method();
+                        }
+                        public class Middle {
+                            private Middle() {}
+                            public abstract class Inner implements GenericInterface<O> {
+                                private Inner() {}
+                            }
+                        }
+                    }
+                """
+            ),
+            kotlin(
+                """
+                    package test.pkg
+
+                    class Outer<O> private constructor() {
+                        interface GenericInterface<T> {
+                            fun method(): T
+                        }
+                        inner class Middle private constructor() {
+                            abstract inner class Inner(o: O): GenericInterface<O>
+                        }
+                    }
+                """
+            ),
+        ) {
+            val oTypeParameter =
+                codebase.assertClass("test.pkg.Outer").typeParameterList().typeParameters().single()
+            val implementsType =
+                codebase.assertClass("test.pkg.Outer.Middle.Inner").interfaceTypes().single()
+            val typeArgument = implementsType.arguments.single()
+
+            typeArgument.assertReferencesTypeParameter(oTypeParameter)
+        }
+    }
+
+    @Test
     fun `Test interface no extends list`() {
         runCodebaseTest(
             signature(
@@ -207,11 +390,15 @@
                 """
             ),
         ) {
-            val objectClass = codebase.assertClass("java.lang.Object")
             val fooClass = codebase.assertClass("test.pkg.Foo")
 
-            assertSame(objectClass, fooClass.superClassType()?.asClass())
-            assertSame(objectClass, fooClass.superClass())
+            // Get the super class to force it to be loaded.
+            val fooSuperClass = fooClass.superClass()
+
+            // Now get the object class.
+            val objectClass = codebase.assertClass("java.lang.Object")
+
+            assertSame(objectClass, fooSuperClass)
 
             val interfaceList = fooClass.interfaceTypes().map { it.asClass() }
             assertEquals(emptyList(), interfaceList)
@@ -290,11 +477,15 @@
             val interfaceA = codebase.assertClass("test.pkg.A")
             val interfaceB = codebase.assertClass("test.pkg.B")
             val interfaceC = codebase.assertClass("test.pkg.C")
-            val objectClass = codebase.assertClass("java.lang.Object")
             val fooClass = codebase.assertClass("test.pkg.Foo")
 
-            assertSame(objectClass, fooClass.superClassType()?.asClass())
-            assertSame(objectClass, fooClass.superClass())
+            // Get the super class to force it to be loaded.
+            val fooSuperClass = fooClass.superClass()
+
+            // Now get the object class.
+            val objectClass = codebase.assertClass("java.lang.Object")
+
+            assertSame(objectClass, fooSuperClass)
 
             val interfaceList = fooClass.interfaceTypes().map { it.asClass() }
             assertEquals(listOf(interfaceA, interfaceB, interfaceC), interfaceList)
@@ -487,13 +678,13 @@
         ) {
             val parent = codebase.assertClass("test.pkg.Parent")
             val parentTypeParams = parent.typeParameterList().typeParameters()
-            val m = parentTypeParams[0].toType()
-            val n = parentTypeParams[1].toType()
+            val m = parentTypeParams[0]
+            val n = parentTypeParams[1]
 
             val child = codebase.assertClass("test.pkg.Child")
             val childTypeParams = child.typeParameterList().typeParameters()
-            val x = childTypeParams[0].toType()
-            val y = childTypeParams[1].toType()
+            val x = childTypeParams[0].type()
+            val y = childTypeParams[1].type()
 
             assertEquals(mapOf(m to x, n to y), child.mapTypeVariables(parent))
 
@@ -568,33 +759,36 @@
             )
         ) {
             val c4 = codebase.assertClass("test.pkg.Class4")
-            val i = c4.typeParameterList().typeParameters()[0].toType()
+            val i = c4.typeParameterList().typeParameters()[0]
 
             val c3 = codebase.assertClass("test.pkg.Class3")
             val c3TypeParams = c3.typeParameterList().typeParameters()
-            val g = c3TypeParams[0].toType()
-            val h = c3TypeParams[1].toType()
+            val g = c3TypeParams[0]
+            val gType = g.type()
+            val h = c3TypeParams[1]
 
             val c2 = codebase.assertClass("test.pkg.Class2")
             val c2TypeParams = c2.typeParameterList().typeParameters()
-            val d = c2TypeParams[0].toType()
-            val e = c2TypeParams[1].toType()
-            val f = c2TypeParams[2].toType()
+            val d = c2TypeParams[0]
+            val dType = d.type()
+            val e = c2TypeParams[1]
+            val f = c2TypeParams[2]
+            val fType = f.type()
 
             val c1 = codebase.assertClass("test.pkg.Class1")
             val c1TypeParams = c1.typeParameterList().typeParameters()
-            val a = c1TypeParams[0].toType()
-            val b = c1TypeParams[1].toType()
-            val c = c1TypeParams[2].toType()
+            val aType = c1TypeParams[0].type()
+            val bType = c1TypeParams[1].type()
+            val cType = c1TypeParams[2].type()
 
-            assertEquals(mapOf(i to g), c3.mapTypeVariables(c4))
+            assertEquals(mapOf(i to gType), c3.mapTypeVariables(c4))
 
-            assertEquals(mapOf(g to d, h to f), c2.mapTypeVariables(c3))
-            assertEquals(mapOf(i to d), c2.mapTypeVariables(c4))
+            assertEquals(mapOf(g to dType, h to fType), c2.mapTypeVariables(c3))
+            assertEquals(mapOf(i to dType), c2.mapTypeVariables(c4))
 
-            assertEquals(mapOf(d to b, e to c, f to a), c1.mapTypeVariables(c2))
-            assertEquals(mapOf(g to b, h to a), c1.mapTypeVariables(c3))
-            assertEquals(mapOf(i to b), c1.mapTypeVariables(c4))
+            assertEquals(mapOf(d to bType, e to cType, f to aType), c1.mapTypeVariables(c2))
+            assertEquals(mapOf(g to bType, h to aType), c1.mapTypeVariables(c3))
+            assertEquals(mapOf(i to bType), c1.mapTypeVariables(c4))
         }
     }
 
@@ -654,19 +848,23 @@
         ) {
             val grandparent = codebase.assertClass("test.pkg.Grandparent")
             val grandparentTypeParams = grandparent.typeParameterList().typeParameters()
-            val a = grandparentTypeParams[0].toType()
-            val b = grandparentTypeParams[1].toType()
+            val a = grandparentTypeParams[0]
+            val b = grandparentTypeParams[1]
 
             val parent = codebase.assertClass("test.pkg.Parent")
-            val t = parent.typeParameterList().typeParameters()[0].toType()
+            val t = parent.typeParameterList().typeParameters()[0]
+            val tType = t.type()
 
             val child = codebase.assertClass("test.pkg.Child")
 
-            val erasedParentType = (parent.toType() as ClassTypeItem).duplicate(null, emptyList())
-            assertEquals(mapOf(a to t, b to erasedParentType), parent.mapTypeVariables(grandparent))
-            assertEquals(mapOf(t to child.toType()), child.mapTypeVariables(parent))
+            val erasedParentType = parent.type().duplicate(null, emptyList())
             assertEquals(
-                mapOf(a to child.toType(), b to erasedParentType),
+                mapOf(a to tType, b to erasedParentType),
+                parent.mapTypeVariables(grandparent)
+            )
+            assertEquals(mapOf(t to child.type()), child.mapTypeVariables(parent))
+            assertEquals(
+                mapOf(a to child.type(), b to erasedParentType),
                 child.mapTypeVariables(grandparent)
             )
         }
@@ -738,29 +936,31 @@
         ) {
             val i3 = codebase.assertClass("test.pkg.Interface3")
             val i3TypeParams = i3.typeParameterList().typeParameters()
-            val g = i3TypeParams[0].toType()
-            val h = i3TypeParams[1].toType()
+            val g = i3TypeParams[0]
+            val h = i3TypeParams[1]
 
             val i2 = codebase.assertClass("test.pkg.Interface2")
             val i2TypeParams = i2.typeParameterList().typeParameters()
-            val e = i2TypeParams[0].toType()
-            val f = i2TypeParams[1].toType()
+            val e = i2TypeParams[0]
+            val eType = e.type()
+            val f = i2TypeParams[1]
+            val fType = f.type()
 
             val i1 = codebase.assertClass("test.pkg.Interface1")
             val i1TypeParams = i1.typeParameterList().typeParameters()
-            val c = i1TypeParams[0].toType()
-            val d = i1TypeParams[1].toType()
+            val c = i1TypeParams[0]
+            val d = i1TypeParams[1]
 
             val cls = codebase.assertClass("test.pkg.Class")
             val clsTypeParams = cls.typeParameterList().typeParameters()
-            val a = clsTypeParams[0].toType()
-            val b = clsTypeParams[1].toType()
+            val aType = clsTypeParams[0].type()
+            val bType = clsTypeParams[1].type()
 
-            assertEquals(mapOf(c to a, d to b), cls.mapTypeVariables(i1))
+            assertEquals(mapOf(c to aType, d to bType), cls.mapTypeVariables(i1))
 
-            assertEquals(mapOf(g to e, h to f), i2.mapTypeVariables(i3))
-            assertEquals(mapOf(e to b, f to a), cls.mapTypeVariables(i2))
-            assertEquals(mapOf(g to b, h to a), cls.mapTypeVariables(i3))
+            assertEquals(mapOf(g to eType, h to fType), i2.mapTypeVariables(i3))
+            assertEquals(mapOf(e to bType, f to aType), cls.mapTypeVariables(i2))
+            assertEquals(mapOf(g to bType, h to aType), cls.mapTypeVariables(i3))
         }
     }
 
@@ -829,31 +1029,33 @@
             )
         ) {
             val root = codebase.assertClass("test.pkg.Root")
-            val t = root.typeParameterList().typeParameters()[0].toType()
+            val t = root.typeParameterList().typeParameters()[0]
 
             val i1 = codebase.assertClass("test.pkg.Interface1")
-            val t1 = i1.typeParameterList().typeParameters()[0].toType()
+            val t1 = i1.typeParameterList().typeParameters()[0]
+            val t1Type = t1.type()
 
             val i2 = codebase.assertClass("test.pkg.Interface2")
-            val t2 = i2.typeParameterList().typeParameters()[0].toType()
+            val t2 = i2.typeParameterList().typeParameters()[0]
+            val t2Type = t2.type()
 
             val child = codebase.assertClass("test.pkg.Child")
             val childParameterList = child.typeParameterList().typeParameters()
-            val x = childParameterList[0].toType()
-            val y = childParameterList[1].toType()
+            val xType = childParameterList[0].type()
+            val yType = childParameterList[1].type()
 
-            assertEquals(mapOf(t to t1), i1.mapTypeVariables(root))
-            assertEquals(mapOf(t to t2), i2.mapTypeVariables(root))
+            assertEquals(mapOf(t to t1Type), i1.mapTypeVariables(root))
+            assertEquals(mapOf(t to t2Type), i2.mapTypeVariables(root))
             assertEquals(
-                mapOf(t1 to x),
+                mapOf(t1 to xType),
                 child.mapTypeVariables(i1),
             )
             assertEquals(
-                mapOf(t2 to y),
+                mapOf(t2 to yType),
                 child.mapTypeVariables(i2),
             )
             assertEquals(
-                mapOf(t to x),
+                mapOf(t to xType),
                 child.mapTypeVariables(root),
             )
         }
@@ -899,24 +1101,71 @@
                         public class Inner {}
                     }
                 """
+            ),
+            signature(
+                """
+                    // Signature format: 5.0
+                    package test.pkg {
+                      public class Outer<T> {
+                      }
+                      public class Outer.Inner {
+                      }
+                    }
+                """
             )
         ) {
             val innerClass = codebase.assertClass("test.pkg.Outer.Inner")
             val outerClass = codebase.assertClass("test.pkg.Outer")
             val outerClassParameter = outerClass.typeParameterList().typeParameters().single()
 
-            val innerType = innerClass.toType()
+            val innerType = innerClass.type()
             assertThat(innerType).isInstanceOf(ClassTypeItem::class.java)
-            assertThat((innerType as ClassTypeItem).qualifiedName).isEqualTo("test.pkg.Outer.Inner")
+            assertThat(innerType.qualifiedName).isEqualTo("test.pkg.Outer.Inner")
 
             val outerType = innerType.outerClassType
             assertThat(outerType).isNotNull()
             assertThat(outerType!!.qualifiedName).isEqualTo("test.pkg.Outer")
 
-            val outerClassVariable = outerType.parameters.single()
-            assertThat(outerClassVariable).isInstanceOf(VariableTypeItem::class.java)
+            val outerClassVariable = outerType.arguments.single()
+            outerClassVariable.assertReferencesTypeParameter(outerClassParameter)
             assertThat((outerClassVariable as VariableTypeItem).name).isEqualTo("T")
-            assertThat(outerClassVariable.asTypeParameter).isEqualTo(outerClassParameter)
+        }
+    }
+
+    @Test
+    fun `Check TypeParameterItem is not a ClassItem`() {
+        runCodebaseTest(
+            signature(
+                """
+                    // Signature format: 5.0
+                    package test.pkg {
+                      public class Generic<T> {
+                      }
+                    }
+                """
+            ),
+            java(
+                """
+                    package test.pkg;
+                    public class Generic<T> {
+                    }
+                """
+            ),
+            kotlin(
+                """
+                    package test.pkg
+                    class Generic<T>
+                """
+            )
+        ) {
+            val genericClass = codebase.assertClass("test.pkg.Generic")
+            val typeParameter = genericClass.typeParameterList().typeParameters().single()
+
+            assertThat(genericClass).isInstanceOf(ClassItem::class.java)
+            assertThat(genericClass).isNotInstanceOf(TypeParameterItem::class.java)
+
+            assertThat(typeParameter).isInstanceOf(TypeParameterItem::class.java)
+            assertThat(typeParameter).isNotInstanceOf(ClassItem::class.java)
         }
     }
 }
diff --git a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/codebase/ParameterizedFindClassTest.kt b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/codebase/ParameterizedFindClassTest.kt
new file mode 100644
index 0000000..ba8dce5
--- /dev/null
+++ b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/codebase/ParameterizedFindClassTest.kt
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.model.testsuite.codebase
+
+import com.android.tools.metalava.model.ClassItem
+import com.android.tools.metalava.model.testsuite.BaseModelTest
+import com.android.tools.metalava.testing.java
+import com.android.tools.metalava.testing.kotlin
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertSame
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+class ParameterizedFindClassTest : BaseModelTest() {
+
+    @Parameterized.Parameter(1) lateinit var params: TestParams
+
+    data class TestParams(
+        val className: String,
+        val expectedFound: Boolean,
+        /**
+         * This is only tested when `true` as even though the class might be unknown some models
+         * that work with partial information, e.g. text, will fabricate an instance just in case it
+         * was real.
+         */
+        val expectedResolved: Boolean = expectedFound,
+    ) {
+        override fun toString(): String {
+            return className
+        }
+    }
+
+    companion object {
+        private val params =
+            listOf(
+                TestParams(
+                    className = "test.pkg.Foo",
+                    expectedFound = true,
+                ),
+                TestParams(
+                    className = "test.pkg.Unknown",
+                    expectedFound = false,
+                ),
+                TestParams(
+                    // Test to make sure that a class whose name does not match the file name will
+                    // be found.
+                    className = "test.pkg.SecondInFile",
+                    expectedFound = true,
+                ),
+                // The following classes will be explicitly loaded. Although these are used
+                // implicitly the behavior differs between models so is hard to test. By specifying
+                // them explicitly it makes the tests more consistent.
+                TestParams(
+                    className = "java.lang.Object",
+                    expectedFound = true,
+                ),
+                TestParams(
+                    className = "java.lang.Throwable",
+                    expectedFound = true,
+                ),
+                // The following classes are implicitly used, directly, or indirectly and are tested
+                // to check that the implicit use does not accidentally include them when they
+                // should not. However, they should all be resolvable.
+                TestParams(
+                    className = "java.lang.annotation.Annotation",
+                    expectedFound = false,
+                    expectedResolved = true,
+                ),
+                TestParams(
+                    className = "java.lang.Enum",
+                    expectedFound = false,
+                    expectedResolved = true,
+                ),
+                TestParams(
+                    className = "java.lang.Comparable",
+                    expectedFound = false,
+                    expectedResolved = true,
+                ),
+                // The following should not be used implicitly by anything.
+                TestParams(
+                    className = "java.io.File",
+                    expectedFound = false,
+                    expectedResolved = true,
+                ),
+            )
+
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0},{1}")
+        fun data(): Collection<Array<Any>> {
+            return crossProduct(params)
+        }
+    }
+
+    private fun assertFound(className: String, expectedFound: Boolean, foundClass: ClassItem?) {
+        if (expectedFound) {
+            assertNotNull(foundClass, message = "$className should exist")
+        } else {
+            assertNull(foundClass, message = "$className should not exist")
+        }
+    }
+
+    @Test
+    fun `test findClass()`() {
+        runCodebaseTest(
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                      public class Foo {
+                        method public Object foo(Throwable) throws Throwable;
+                      }
+                      public class SecondInFile {
+                      }
+                    }
+                """
+            ),
+            java(
+                """
+                    package test.pkg;
+                    public class Foo {
+                        private Foo() {}
+                        public Object foo(Throwable t) throws Throwable {throw new Throwable();}
+                    }
+                    public class SecondInFile {
+                    }
+                """
+            ),
+            kotlin(
+                """
+                    package test.pkg
+                    class Foo
+                    private constructor() {
+                        @Throws(Throwable::class)
+                        fun foo(t: Throwable): Any {throw Throwable()}
+                    }
+                    class SecondInFile {
+                    }
+                """
+            ),
+        ) {
+            val fooMethod = codebase.assertClass("test.pkg.Foo").methods().single()
+
+            // Force loading of the Object classes by resolving the return type which is
+            // java.lang.Object.
+            fooMethod.returnType().asClass()
+
+            // Force loading of the Throwable classes by resolving the parameter's type which is
+            // java.lang.Object.
+            fooMethod.parameters().single().type().asClass()
+
+            val className = params.className
+            val foundClass = codebase.findClass(className)
+            assertFound(className, params.expectedFound, foundClass)
+
+            val resolvedClass = codebase.resolveClass(className)
+            if (foundClass == null) {
+                // If the class was not found then resolving might have found it.
+                if (params.expectedResolved) {
+                    assertNotNull(resolvedClass, message = "expected to resolve $className")
+                }
+
+                // If the class was resolved then it must now be found.
+                if (resolvedClass != null) {
+                    val foundClassAfterResolving = codebase.findClass(className)
+                    assertSame(
+                        resolvedClass,
+                        foundClassAfterResolving,
+                        message = "could not find $className even though it was previously resolved"
+                    )
+                }
+            } else {
+                // If the class was found then it must be resolved to the same class.
+                assertSame(
+                    foundClass,
+                    resolvedClass,
+                    message = "could not resolve $className even though it was previously found"
+                )
+            }
+        }
+    }
+}
diff --git a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/constructoritem/CommonConstructorItemTest.kt b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/constructoritem/CommonConstructorItemTest.kt
new file mode 100644
index 0000000..31f44d1
--- /dev/null
+++ b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/constructoritem/CommonConstructorItemTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.model.testsuite.constructoritem
+
+import com.android.tools.metalava.model.MethodItem
+import com.android.tools.metalava.model.testsuite.BaseModelTest
+import com.android.tools.metalava.testing.java
+import com.android.tools.metalava.testing.kotlin
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/** Common tests for implementations of [MethodItem]. */
+@RunWith(Parameterized::class)
+class CommonConstructorItemTest : BaseModelTest() {
+
+    @Test
+    fun `Test access type parameter of outer class`() {
+        runCodebaseTest(
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                      public class Outer<O> {
+                      }
+                      public class Outer.Middle {
+                      }
+                      public abstract class Outer.Middle.Inner {
+                        ctor public Inner(O);
+                      }
+                    }
+                """
+            ),
+            java(
+                """
+                    package test.pkg;
+
+                    public class Outer<O> {
+                        private Outer() {}
+
+                        public class Middle {
+                            private Middle() {}
+                            public class Inner {
+                                public Inner(O o) {}
+                            }
+                        }
+                    }
+                """
+            ),
+            kotlin(
+                """
+                    package test.pkg
+
+                    class Outer<O> private constructor() {
+                        inner class Middle private constructor() {
+                            abstract inner class Inner(o: O) {
+                            }
+                        }
+                    }
+                """
+            ),
+        ) {
+            val oTypeParameter =
+                codebase.assertClass("test.pkg.Outer").typeParameterList().typeParameters().single()
+            val constructorType =
+                codebase
+                    .assertClass("test.pkg.Outer.Middle.Inner")
+                    .constructors()
+                    .first()
+                    .parameters()
+                    .last()
+                    .type()
+
+            constructorType.assertReferencesTypeParameter(oTypeParameter)
+        }
+    }
+}
diff --git a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/fielditem/CommonFieldItemTest.kt b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/fielditem/CommonFieldItemTest.kt
index 62e3291..3b42269 100644
--- a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/fielditem/CommonFieldItemTest.kt
+++ b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/fielditem/CommonFieldItemTest.kt
@@ -31,6 +31,50 @@
 class CommonFieldItemTest : BaseModelTest() {
 
     @Test
+    fun `Test access type parameter of outer class`() {
+        runCodebaseTest(
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                      public class Outer<O> {
+                      }
+                      public class Outer.Middle {
+                      }
+                      public class Outer.Middle.Inner {
+                        field public O field;
+                      }
+                    }
+                """
+            ),
+            java(
+                """
+                    package test.pkg;
+
+                    public class Outer<O> {
+                        private Outer() {}
+
+                        public class Middle {
+                            private Middle() {}
+                            public class Inner {
+                                private Inner() {}
+                                public O field;
+                            }
+                        }
+                    }
+                """
+            ),
+        ) {
+            val oTypeParameter =
+                codebase.assertClass("test.pkg.Outer").typeParameterList().typeParameters().single()
+            val fieldType =
+                codebase.assertClass("test.pkg.Outer.Middle.Inner").assertField("field").type()
+
+            fieldType.assertReferencesTypeParameter(oTypeParameter)
+        }
+    }
+
+    @Test
     fun `Test handling of Float MIN_NORMAL`() {
         runCodebaseTest(
             signature(
diff --git a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/fielditem/SourceFieldItemTest.kt b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/fielditem/SourceFieldItemTest.kt
index 7476bf0..94ec9a1 100644
--- a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/fielditem/SourceFieldItemTest.kt
+++ b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/fielditem/SourceFieldItemTest.kt
@@ -231,4 +231,40 @@
             assertNotNull(fieldItem.initialValue(false))
         }
     }
+
+    @Test
+    fun `test duplicate() for fielditem`() {
+        runSourceCodebaseTest(
+            java(
+                """
+                    package test.pkg;
+
+                    /** @doconly Some docs here */
+                    public class Test {
+                        public static final int Field = 7;
+                    }
+
+                    /** @hide */
+                    public class Target {}
+                """
+            ),
+        ) {
+            val classItem = codebase.assertClass("test.pkg.Test")
+            val targetClassItem = codebase.assertClass("test.pkg.Target")
+            val fieldItem = classItem.assertField("Field")
+
+            val duplicateField = fieldItem.duplicate(targetClassItem)
+
+            assertEquals(
+                fieldItem.modifiers.getVisibilityLevel(),
+                duplicateField.modifiers.getVisibilityLevel()
+            )
+            assertEquals(true, fieldItem.modifiers.equivalentTo(duplicateField.modifiers))
+            assertEquals(true, duplicateField.hidden)
+            assertEquals(false, duplicateField.docOnly)
+            assertEquals(fieldItem.type(), duplicateField.type())
+            assertEquals(fieldItem.initialValue(), duplicateField.initialValue())
+            assertEquals(classItem, duplicateField.inheritedFrom)
+        }
+    }
 }
diff --git a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/methoditem/CommonMethodItemTest.kt b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/methoditem/CommonMethodItemTest.kt
index 2c85e87..bf05594 100644
--- a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/methoditem/CommonMethodItemTest.kt
+++ b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/methoditem/CommonMethodItemTest.kt
@@ -16,10 +16,14 @@
 
 package com.android.tools.metalava.model.testsuite.methoditem
 
+import com.android.tools.metalava.model.JAVA_LANG_THROWABLE
+import com.android.tools.metalava.model.MethodItem
 import com.android.tools.metalava.model.testsuite.BaseModelTest
 import com.android.tools.metalava.testing.java
+import com.android.tools.metalava.testing.kotlin
 import kotlin.test.assertEquals
 import kotlin.test.assertNotEquals
+import kotlin.test.assertNull
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -29,6 +33,66 @@
 class CommonMethodItemTest : BaseModelTest() {
 
     @Test
+    fun `Test access type parameter of outer class`() {
+        runCodebaseTest(
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                      public class Outer<O> {
+                      }
+                      public class Outer.Middle {
+                      }
+                      public abstract class Outer.Middle.Inner {
+                        method public abstract O method();
+                      }
+                    }
+                """
+            ),
+            java(
+                """
+                    package test.pkg;
+
+                    public class Outer<O> {
+                        private Outer() {}
+
+                        public class Middle {
+                            private Middle() {}
+                            public class Inner {
+                                private Inner() {}
+                                public abstract O method();
+                            }
+                        }
+                    }
+                """
+            ),
+            kotlin(
+                """
+                    package test.pkg
+
+                    class Outer<O> private constructor() {
+                        inner class Middle private constructor() {
+                            abstract inner class Inner private constructor() {
+                                abstract fun method(): O
+                            }
+                        }
+                    }
+                """
+            ),
+        ) {
+            val oTypeParameter =
+                codebase.assertClass("test.pkg.Outer").typeParameterList().typeParameters().single()
+            val methodType =
+                codebase
+                    .assertClass("test.pkg.Outer.Middle.Inner")
+                    .assertMethod("method", "")
+                    .type()
+
+            methodType.assertReferencesTypeParameter(oTypeParameter)
+        }
+    }
+
+    @Test
     fun `MethodItem type`() {
         runCodebaseTest(
             signature(
@@ -107,7 +171,7 @@
                 java(
                     """
                         package test.pkg;
-    
+
                         public class Base {
                             public Base() {}
                         }
@@ -116,7 +180,7 @@
                 java(
                     """
                         package test.pkg;
-    
+
                         public class Test extends Base {
                             public Test() {}
                         }
@@ -154,7 +218,7 @@
                 java(
                     """
                         package test.pkg;
-    
+
                         public class Base {
                             public Base() {}
                             public void foo() {}
@@ -164,7 +228,7 @@
                 java(
                     """
                         package test.pkg;
-    
+
                         public class Test extends Base {
                             public Test() {}
                             public void foo() {}
@@ -216,4 +280,76 @@
             assertNotEquals(numBounds, strBounds)
         }
     }
+
+    @Test
+    fun `Test throws method type parameter extends Throwable`() {
+        runCodebaseTest(
+            java(
+                """
+                    package test.pkg;
+
+                    @SuppressWarnings("ALL")
+                    public final class Test {
+                        private Test() {}
+                        public <X extends Throwable> void throwsTypeParameter() throws X {
+                            return null;
+                        }
+                    }
+                """
+            ),
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                      public final class Test {
+                        method public <X extends Throwable> void throwsTypeParameter() throws X;
+                      }
+                    }
+                """
+            ),
+        ) {
+            val methodItem = codebase.assertClass("test.pkg.Test").methods().single()
+            val typeParameterItem = methodItem.typeParameterList().typeParameters().single()
+            val throwsType = methodItem.throwsTypes().single()
+            assertEquals(typeParameterItem, throwsType.typeParameterItem)
+            assertEquals(throwsType.throwableClass?.qualifiedName(), JAVA_LANG_THROWABLE)
+        }
+    }
+
+    @Test
+    fun `Test throws method type parameter does not extend Throwable`() {
+        // This is an error but Metalava should try not to fail on an error.
+        runCodebaseTest(
+            java(
+                """
+                    package test.pkg;
+
+                    @SuppressWarnings("ALL")
+                    public final class Test {
+                        private Test() {}
+                        public <X> void throwsTypeParameter() throws X {
+                            return null;
+                        }
+                    }
+                """
+            ),
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                      public final class Test {
+                        method public <X> void throwsTypeParameter() throws X;
+                      }
+                    }
+                """
+            ),
+        ) {
+            val methodItem = codebase.assertClass("test.pkg.Test").methods().single()
+            val typeParameterItem = methodItem.typeParameterList().typeParameters().single()
+            val throwsType = methodItem.throwsTypes().single()
+            assertEquals(typeParameterItem, throwsType.typeParameterItem)
+            // The type parameter does not extend a throwable type.
+            assertNull(throwsType.throwableClass)
+        }
+    }
 }
diff --git a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/methoditem/CommonParameterItemTest.kt b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/methoditem/CommonParameterItemTest.kt
index 3a7d7fe..9805a0a 100644
--- a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/methoditem/CommonParameterItemTest.kt
+++ b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/methoditem/CommonParameterItemTest.kt
@@ -18,9 +18,11 @@
 
 import com.android.tools.metalava.model.source.SourceLanguage
 import com.android.tools.metalava.model.testsuite.BaseModelTest
+import com.android.tools.metalava.testing.KnownSourceFiles
 import com.android.tools.metalava.testing.java
 import com.android.tools.metalava.testing.kotlin
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -112,4 +114,142 @@
             assertEquals("originallyDeprecated", false, parameterItem.originallyDeprecated)
         }
     }
+
+    @Test
+    fun `Test publicName reports correct name when specified`() {
+        runCodebaseTest(
+            inputSet(
+                signature(
+                    """
+                        // Signature format: 2.0
+                        package test.pkg {
+                          public class Bar {
+                            method public void foo(int baz);
+                          }
+                        }
+                    """
+                ),
+            ),
+            inputSet(
+                KnownSourceFiles.supportParameterName,
+                java(
+                    """
+                        package test.pkg;
+
+                        import androidx.annotation.ParameterName;
+
+                        public class Bar {
+                            public void foo(@ParameterName("baz") int baz) {}
+                        }
+                    """
+                ),
+            ),
+            inputSet(
+                kotlin(
+                    """
+                        package test.pkg
+
+                        class Bar {
+                            fun foo(baz: Int) {}
+                        }
+                    """
+                ),
+            ),
+        ) {
+            val parameterItem =
+                codebase.assertClass("test.pkg.Bar").methods().single().parameters().single()
+            assertEquals("name()", "baz", parameterItem.name())
+            assertEquals("publicName()", "baz", parameterItem.publicName())
+        }
+    }
+
+    @Test
+    fun `Test publicName reports correct name when not specified`() {
+        runCodebaseTest(
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                      public class Bar {
+                        method public void foo(int);
+                      }
+                    }
+                """
+            ),
+            java(
+                """
+                    package test.pkg;
+
+                    public class Bar {
+                        public void foo(int baz) {}
+                    }
+                """
+            ),
+            // Kotlin treats all parameter names as public.
+        ) {
+            val parameterItem =
+                codebase.assertClass("test.pkg.Bar").methods().single().parameters().single()
+            assertNull("publicName()", parameterItem.publicName())
+        }
+    }
+
+    @Test
+    fun `Test publicName reports correct name when called on binary class - Object#equals`() {
+        runCodebaseTest(
+            java(
+                """
+                    package test.pkg;
+
+                    public abstract class Bar {
+                    }
+                """
+            ),
+            // No need to check any other sources as the source is not being tested, only used to
+            // trigger the test run.
+        ) {
+            val parameterItem =
+                codebase
+                    .assertClass("java.lang.Object")
+                    .assertMethod("equals", "java.lang.Object")
+                    .parameters()
+                    .single()
+            // For some reason Object.equals(Object obj) provides the actual parameter name.
+            // Probably, because it was compiled with a late enough version of javac, and/or with
+            // the appropriate options to record the parameter name.
+            assertEquals("name()", "obj", parameterItem.name())
+            assertEquals("publicName()", "obj", parameterItem.publicName())
+        }
+    }
+
+    @Test
+    fun `Test publicName reports correct name when called on binary class - ViewGroup#onLayout`() {
+        runCodebaseTest(
+            java(
+                """
+                    package test.pkg;
+
+                    public abstract class Bar extends android.view.ViewGroup {
+                    }
+                """
+            ),
+            // No need to check any other sources as the source is not being tested, only used to
+            // trigger the test run.
+        ) {
+            val parameterItems =
+                codebase
+                    .assertClass("android.view.ViewGroup")
+                    .assertMethod("onLayout", "boolean, int, int, int, int")
+                    .parameters()
+            // For some reason ViewGroup.onLayout(boolean, int, int, int, int) does not provide the
+            // actual parameter name. Probably, because it was compiled with an older version of
+            // javac, and/or without the appropriate options to record the parameter name.
+            val expectedNames = listOf("p", "p1", "p2", "p3", "p4")
+            for (i in parameterItems.indices) {
+                val parameterItem = parameterItems[i]
+                val expectedName = expectedNames[i]
+                assertEquals("$i:name()", expectedName, parameterItem.name())
+                assertNull("$i:publicName()$parameterItem", parameterItem.publicName())
+            }
+        }
+    }
 }
diff --git a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/methoditem/SourceMethodItemTest.kt b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/methoditem/SourceMethodItemTest.kt
new file mode 100644
index 0000000..cdc1fe8
--- /dev/null
+++ b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/methoditem/SourceMethodItemTest.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.model.testsuite.methoditem
+
+import com.android.tools.metalava.model.testsuite.BaseModelTest
+import com.android.tools.metalava.testing.java
+import kotlin.test.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/** Common tests for implementations of [MethodItem] for source based models. */
+@RunWith(Parameterized::class)
+class SourceMethodItemTest : BaseModelTest() {
+    @Test
+    fun `test duplicate() for methoditem`() {
+        runSourceCodebaseTest(
+            java(
+                """
+                    package test.pkg;
+
+                    import java.io.IOException;
+
+                    /** @doconly Some docs here */
+                    public class Test<A,B>  {
+                        public final void foo(A a, B b) throws IOException {}
+
+                        public final <C,D extends Number> void foo1(C a,D d) {}
+                    }
+
+                    /** @hide */
+                    public class Target<M,String> extends Test<M,String>{}
+                """
+            ),
+        ) {
+            val classItem = codebase.assertClass("test.pkg.Test")
+            val targetClassItem = codebase.assertClass("test.pkg.Target")
+            val methodItem = classItem.methods().first()
+            val methodItem1 = classItem.methods().last()
+
+            val duplicateMethod = methodItem.duplicate(targetClassItem)
+            val duplicateMethod1 = methodItem1.duplicate(targetClassItem)
+
+            assertEquals(
+                methodItem.modifiers.getVisibilityLevel(),
+                duplicateMethod.modifiers.getVisibilityLevel()
+            )
+            assertEquals(true, methodItem.modifiers.equivalentTo(duplicateMethod.modifiers))
+            assertEquals(true, duplicateMethod.hidden)
+            assertEquals(false, duplicateMethod.docOnly)
+            assertEquals("void", duplicateMethod.returnType().toTypeString())
+            assertEquals(
+                listOf("A", "B"),
+                duplicateMethod.parameters().map { it.type().toTypeString() }
+            )
+            assertEquals(
+                methodItem.typeParameterList().typeParameters(),
+                duplicateMethod.typeParameterList().typeParameters()
+            )
+            assertEquals(methodItem.throwsTypes(), duplicateMethod.throwsTypes())
+            assertEquals(classItem, duplicateMethod.inheritedFrom)
+
+            assertEquals(
+                methodItem1.modifiers.getVisibilityLevel(),
+                duplicateMethod1.modifiers.getVisibilityLevel()
+            )
+            assertEquals(true, methodItem1.modifiers.equivalentTo(duplicateMethod1.modifiers))
+            assertEquals(true, duplicateMethod1.hidden)
+            assertEquals(false, duplicateMethod1.docOnly)
+            assertEquals("void", duplicateMethod.returnType().toTypeString())
+            assertEquals(
+                listOf("C", "D"),
+                duplicateMethod1.parameters().map { it.type().toTypeString() }
+            )
+            assertEquals(
+                methodItem1.typeParameterList().typeParameters(),
+                duplicateMethod1.typeParameterList().typeParameters()
+            )
+            assertEquals(methodItem1.throwsTypes(), duplicateMethod1.throwsTypes())
+            assertEquals(classItem, duplicateMethod1.inheritedFrom)
+        }
+    }
+
+    @Test
+    fun `test inherited methods`() {
+        runSourceCodebaseTest(
+            java(
+                """
+                    package test.pkg;
+
+                    import java.io.IOException;
+
+                    /** @doconly Some docs here */
+                    public class Test<A,B>  {
+                        public final void foo(A a, B b) throws IOException {}
+
+                        public final <C,D extends Number> void foo1(C a,D d) {}
+                    }
+
+                    /** @hide */
+                    public class Target<M,String> extends Test<M,String> {}
+                """
+            ),
+        ) {
+            val classItem = codebase.assertClass("test.pkg.Test")
+            val targetClassItem = codebase.assertClass("test.pkg.Target")
+            val methodItem = classItem.methods().first()
+            val methodItem1 = classItem.methods().last()
+
+            val inheritedMethod = targetClassItem.inheritMethodFromNonApiAncestor(methodItem)
+            val inheritedMethod1 = targetClassItem.inheritMethodFromNonApiAncestor(methodItem1)
+
+            assertEquals(
+                methodItem.modifiers.getVisibilityLevel(),
+                inheritedMethod.modifiers.getVisibilityLevel()
+            )
+            assertEquals(true, methodItem.modifiers.equivalentTo(inheritedMethod.modifiers))
+            assertEquals(false, inheritedMethod.hidden)
+            assertEquals(false, inheritedMethod.docOnly)
+            assertEquals("void", inheritedMethod.returnType().toTypeString())
+            assertEquals(
+                listOf("M", "String"),
+                inheritedMethod.parameters().map { it.type().toTypeString() }
+            )
+            assertEquals(
+                methodItem.typeParameterList().typeParameters(),
+                inheritedMethod.typeParameterList().typeParameters()
+            )
+            assertEquals(methodItem.throwsTypes(), inheritedMethod.throwsTypes())
+            assertEquals(classItem, inheritedMethod.inheritedFrom)
+
+            assertEquals(
+                methodItem1.modifiers.getVisibilityLevel(),
+                inheritedMethod1.modifiers.getVisibilityLevel()
+            )
+            assertEquals(true, methodItem1.modifiers.equivalentTo(inheritedMethod1.modifiers))
+            assertEquals(false, inheritedMethod1.hidden)
+            assertEquals(false, inheritedMethod1.docOnly)
+            assertEquals(methodItem1.returnType(), inheritedMethod1.returnType())
+            assertEquals("void", inheritedMethod.returnType().toTypeString())
+            assertEquals(
+                listOf("C", "D"),
+                inheritedMethod1.parameters().map { it.type().toTypeString() }
+            )
+            assertEquals(
+                methodItem1.typeParameterList().typeParameters(),
+                inheritedMethod1.typeParameterList().typeParameters()
+            )
+            assertEquals(methodItem1.throwsTypes(), inheritedMethod1.throwsTypes())
+            assertEquals(classItem, inheritedMethod1.inheritedFrom)
+        }
+    }
+}
diff --git a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/propertyitem/CommonPropertyItemTest.kt b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/propertyitem/CommonPropertyItemTest.kt
index 7e5719a..a1e5c48 100644
--- a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/propertyitem/CommonPropertyItemTest.kt
+++ b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/propertyitem/CommonPropertyItemTest.kt
@@ -29,6 +29,49 @@
 class CommonPropertyItemTest : BaseModelTest() {
 
     @Test
+    fun `Test access type parameter of outer class`() {
+        runCodebaseTest(
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                      public class Outer<O> {
+                      }
+                      public class Outer.Middle {
+                      }
+                      public abstract class Outer.Middle.Inner {
+                        property public abstract O property;
+                      }
+                    }
+                """
+            ),
+            kotlin(
+                """
+                    package test.pkg
+
+                    class Outer<O> private constructor() {
+                        inner class Middle private constructor() {
+                            abstract inner class Inner private constructor() {
+                                abstract val property: O
+                            }
+                        }
+                    }
+                """
+            ),
+        ) {
+            val oTypeParameter =
+                codebase.assertClass("test.pkg.Outer").typeParameterList().typeParameters().single()
+            val propertyType =
+                codebase
+                    .assertClass("test.pkg.Outer.Middle.Inner")
+                    .assertProperty("property")
+                    .type()
+
+            propertyType.assertReferencesTypeParameter(oTypeParameter)
+        }
+    }
+
+    @Test
     fun `Test deprecated getter and setter by annotation`() {
         runCodebaseTest(
             kotlin(
diff --git a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/typeitem/CommonParameterizedTypeItemTest.kt b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/typeitem/CommonParameterizedTypeItemTest.kt
new file mode 100644
index 0000000..fd7a2be
--- /dev/null
+++ b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/typeitem/CommonParameterizedTypeItemTest.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.model.testsuite.typeitem
+
+import com.android.tools.metalava.model.Codebase
+import com.android.tools.metalava.model.TypeItem
+import com.android.tools.metalava.model.testsuite.BaseModelTest
+import com.android.tools.metalava.testing.java
+import com.android.tools.metalava.testing.kotlin
+import kotlin.test.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+class CommonParameterizedTypeItemTest : BaseModelTest() {
+
+    @Parameterized.Parameter(1) lateinit var params: TestParams
+
+    data class TestParams(
+        val javaTypeParameter: String? = null,
+        val javaType: String,
+        val name: String = javaType,
+        val kotlinModifiers: String? = null,
+        val kotlinTypeParameter: String? = null,
+        val kotlinType: String,
+        val expectedAsClassName: String?,
+    ) {
+        fun javaParameter(): String = "$javaType p"
+
+        fun javaTypeParameter(): String = javaTypeParameter ?: ""
+
+        fun kotlinParameter(): String = "${kotlinModifiers?:""} p: $kotlinType"
+
+        fun kotlinTypeParameter(): String = kotlinTypeParameter ?: ""
+
+        override fun toString(): String {
+            return name
+        }
+    }
+
+    companion object {
+        private val params =
+            listOf(
+                TestParams(
+                    javaType = "int",
+                    kotlinType = "Int",
+                    expectedAsClassName = null,
+                ),
+                TestParams(
+                    javaType = "int[]",
+                    kotlinType = "IntArray",
+                    expectedAsClassName = null,
+                ),
+                TestParams(
+                    javaType = "Comparable<String>",
+                    kotlinType = "Comparable<String>",
+                    expectedAsClassName = "java.lang.Comparable",
+                ),
+                TestParams(
+                    javaType = "String[]...",
+                    kotlinModifiers = "vararg",
+                    kotlinType = "Array<String>",
+                    expectedAsClassName = "java.lang.String",
+                ),
+                TestParams(
+                    javaTypeParameter = "<T extends Comparable<T>>",
+                    javaType = "java.util.Map.Entry<String, T>",
+                    kotlinTypeParameter = "<T: Comparable<T>>",
+                    kotlinType = "java.util.Map.Entry<String, T>",
+                    expectedAsClassName = "java.util.Map.Entry",
+                ),
+                TestParams(
+                    javaTypeParameter = "<T>",
+                    javaType = "T",
+                    kotlinTypeParameter = "<T>",
+                    kotlinType = "T",
+                    expectedAsClassName = "java.lang.Object",
+                ),
+                TestParams(
+                    name = "T extends Comparable",
+                    javaTypeParameter = "<T extends Comparable<T>>",
+                    javaType = "T",
+                    kotlinTypeParameter = "<T: Comparable<T>>",
+                    kotlinType = "T",
+                    expectedAsClassName = "java.lang.Comparable",
+                ),
+                TestParams(
+                    javaTypeParameter = "<T extends Comparable<T>>",
+                    javaType = "T[]",
+                    kotlinTypeParameter = "<T: Comparable<T>>",
+                    kotlinType = "Array<T>",
+                    expectedAsClassName = "java.lang.Comparable",
+                ),
+                TestParams(
+                    javaType = "Comparable<Integer>[]",
+                    kotlinType = "Array<Comparable<Int>>",
+                    expectedAsClassName = "java.lang.Comparable",
+                ),
+            )
+
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0},{1}")
+        fun data(): Collection<Array<Any>> {
+            return crossProduct(params)
+        }
+    }
+
+    internal data class TestContext(
+        val codebase: Codebase,
+        val typeItem: TypeItem,
+    )
+
+    private fun runTypeItemTest(test: TestContext.() -> Unit) {
+        runCodebaseTest(
+            signature(
+                """
+                // Signature format: 2.0
+                package test.pkg {
+                    public interface Foo {
+                        method public ${params.javaTypeParameter()} void method(${params.javaParameter()});
+                    }
+                }
+                """
+            ),
+            java(
+                """
+                package test.pkg;
+                public interface Foo {
+                    ${params.javaTypeParameter()} void method(${params.javaParameter()});
+                }
+                """
+            ),
+            kotlin(
+                """
+                package test.pkg
+                interface Foo {
+                    fun ${params.kotlinTypeParameter()} method(${params.kotlinParameter()})
+                }
+                """
+            ),
+        ) {
+            val methodItem = codebase.assertClass("test.pkg.Foo").methods().single()
+            val parameterItem = methodItem.parameters()[0]
+            val typeItem = parameterItem.type()
+            TestContext(
+                    codebase = codebase,
+                    typeItem = typeItem,
+                )
+                .test()
+        }
+    }
+
+    @Test
+    fun `Test asClass`() {
+        runTypeItemTest {
+            assertEquals(params.expectedAsClassName, typeItem.asClass()?.qualifiedName())
+        }
+    }
+}
diff --git a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/typeitem/CommonTypeItemTest.kt b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/typeitem/CommonTypeItemTest.kt
index 1cf9636..b695f1c 100644
--- a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/typeitem/CommonTypeItemTest.kt
+++ b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/typeitem/CommonTypeItemTest.kt
@@ -19,12 +19,14 @@
 import com.android.tools.metalava.model.ArrayTypeItem
 import com.android.tools.metalava.model.ClassTypeItem
 import com.android.tools.metalava.model.PrimitiveTypeItem
+import com.android.tools.metalava.model.ReferenceTypeItem
 import com.android.tools.metalava.model.VariableTypeItem
 import com.android.tools.metalava.model.WildcardTypeItem
 import com.android.tools.metalava.model.testsuite.BaseModelTest
 import com.android.tools.metalava.testing.java
 import com.android.tools.metalava.testing.kotlin
 import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -385,8 +387,8 @@
                 method.parameters().map {
                     val paramType = it.type()
                     assertThat(paramType).isInstanceOf(ClassTypeItem::class.java)
-                    assertThat((paramType as ClassTypeItem).parameters).hasSize(1)
-                    paramType.parameters.single()
+                    assertThat((paramType as ClassTypeItem).arguments).hasSize(1)
+                    paramType.arguments.single()
                 }
             assertThat(wildcardTypes).hasSize(4)
 
@@ -468,14 +470,12 @@
             assertThat(paramTypes).hasSize(2)
 
             val classTypeVariable = paramTypes[0]
-            assertThat(classTypeVariable).isInstanceOf(VariableTypeItem::class.java)
+            classTypeVariable.assertReferencesTypeParameter(classTypeParam)
             assertThat((classTypeVariable as VariableTypeItem).name).isEqualTo("C")
-            assertThat(classTypeVariable.asTypeParameter).isEqualTo(classTypeParam)
 
             val methodTypeVariable = paramTypes[1]
-            assertThat(methodTypeVariable).isInstanceOf(VariableTypeItem::class.java)
+            methodTypeVariable.assertReferencesTypeParameter(methodTypeParam)
             assertThat((methodTypeVariable as VariableTypeItem).name).isEqualTo("M")
-            assertThat(methodTypeVariable.asTypeParameter).isEqualTo(methodTypeParam)
         }
     }
 
@@ -524,25 +524,21 @@
 
             val bar1 = foo.methods().single { it.name() == "bar1" }
             val bar1Return = bar1.returnType()
-            assertThat(bar1Return).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((bar1Return as VariableTypeItem).asTypeParameter).isEqualTo(fooTypeParam)
+            bar1Return.assertReferencesTypeParameter(fooTypeParam)
 
             val bar2 = foo.methods().single { it.name() == "bar2" }
             val bar2TypeParam = bar2.typeParameterList().typeParameters().single()
             val bar2Return = bar2.returnType()
-            assertThat(bar2Return).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((bar2Return as VariableTypeItem).asTypeParameter).isEqualTo(bar2TypeParam)
+            bar2Return.assertReferencesTypeParameter(bar2TypeParam)
 
             val bar3 = foo.methods().single { it.name() == "bar3" }
             val bar3Return = bar3.returnType()
-            assertThat(bar3Return).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((bar3Return as VariableTypeItem).asTypeParameter).isEqualTo(fooTypeParam)
+            bar3Return.assertReferencesTypeParameter(fooTypeParam)
 
             val bar4 = foo.methods().single { it.name() == "bar4" }
             val bar4TypeParam = bar4.typeParameterList().typeParameters().single()
             val bar4Return = bar4.returnType()
-            assertThat(bar4Return).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((bar4Return as VariableTypeItem).asTypeParameter).isEqualTo(bar4TypeParam)
+            bar4Return.assertReferencesTypeParameter(bar4TypeParam)
         }
     }
 
@@ -591,25 +587,21 @@
 
             val bar1 = foo.methods().single { it.name() == "bar1" }
             val bar1Param = bar1.parameters().single().type()
-            assertThat(bar1Param).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((bar1Param as VariableTypeItem).asTypeParameter).isEqualTo(fooParam)
+            bar1Param.assertReferencesTypeParameter(fooParam)
 
             val bar2 = foo.methods().single { it.name() == "bar2" }
             val bar2TypeParam = bar2.typeParameterList().typeParameters().single()
             val bar2Param = bar2.parameters().single().type()
-            assertThat(bar2Param).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((bar2Param as VariableTypeItem).asTypeParameter).isEqualTo(bar2TypeParam)
+            bar2Param.assertReferencesTypeParameter(bar2TypeParam)
 
             val bar3 = foo.methods().single { it.name() == "bar3" }
             val bar3Param = bar3.parameters().single().type()
-            assertThat(bar3Param).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((bar3Param as VariableTypeItem).asTypeParameter).isEqualTo(fooParam)
+            bar3Param.assertReferencesTypeParameter(fooParam)
 
             val bar4 = foo.methods().single { it.name() == "bar4" }
             val bar4TypeParam = bar4.typeParameterList().typeParameters().single()
             val bar4Param = bar4.parameters().single().type()
-            assertThat(bar4Param).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((bar4Param as VariableTypeItem).asTypeParameter).isEqualTo(bar4TypeParam)
+            bar4Param.assertReferencesTypeParameter(bar4TypeParam)
         }
     }
 
@@ -649,8 +641,7 @@
             val fooParam = foo.typeParameterList().typeParameters().single()
 
             val fieldType = foo.fields().single { it.name() == "foo" }.type()
-            assertThat(fieldType).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((fieldType as VariableTypeItem).asTypeParameter).isEqualTo(fooParam)
+            fieldType.assertReferencesTypeParameter(fooParam)
         }
     }
 
@@ -683,8 +674,7 @@
             val fooParam = foo.typeParameterList().typeParameters().single()
 
             val propertyType = foo.properties().single { it.name() == "foo" }.type()
-            assertThat(propertyType).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((propertyType as VariableTypeItem).asTypeParameter).isEqualTo(fooParam)
+            propertyType.assertReferencesTypeParameter(fooParam)
         }
     }
 
@@ -737,22 +727,22 @@
             assertThat(stringType).isInstanceOf(ClassTypeItem::class.java)
             assertThat((stringType as ClassTypeItem).qualifiedName).isEqualTo("java.lang.String")
             assertThat(stringType.className).isEqualTo("String")
-            assertThat(stringType.parameters).isEmpty()
+            assertThat(stringType.arguments).isEmpty()
 
             // List<String>
             val stringListType = paramTypes[1]
             assertThat(stringListType).isInstanceOf(ClassTypeItem::class.java)
             assertThat((stringListType as ClassTypeItem).qualifiedName).isEqualTo("java.util.List")
             assertThat(stringListType.className).isEqualTo("List")
-            assertThat(stringListType.parameters).hasSize(1)
-            assertThat(stringListType.parameters.single().isString()).isTrue()
+            assertThat(stringListType.arguments).hasSize(1)
+            assertThat(stringListType.arguments.single().isString()).isTrue()
 
             // List<String[]> / List<Array<String>>
             val arrayListType = paramTypes[2]
             assertThat(arrayListType).isInstanceOf(ClassTypeItem::class.java)
             assertThat((arrayListType as ClassTypeItem).qualifiedName).isEqualTo("java.util.List")
-            assertThat(arrayListType.parameters).hasSize(1)
-            val arrayType = arrayListType.parameters.single()
+            assertThat(arrayListType.arguments).hasSize(1)
+            val arrayType = arrayListType.arguments.single()
             assertThat(arrayType).isInstanceOf(ArrayTypeItem::class.java)
             assertThat((arrayType as ArrayTypeItem).componentType.isString()).isTrue()
 
@@ -760,11 +750,11 @@
             val mapType = paramTypes[3]
             assertThat(mapType).isInstanceOf(ClassTypeItem::class.java)
             assertThat((mapType as ClassTypeItem).qualifiedName).isEqualTo("java.util.Map")
-            assertThat(mapType.parameters).hasSize(2)
-            val mapKeyType = mapType.parameters.first()
+            assertThat(mapType.arguments).hasSize(2)
+            val mapKeyType = mapType.arguments.first()
             assertThat(mapKeyType).isInstanceOf(ClassTypeItem::class.java)
             assertThat((mapKeyType as ClassTypeItem).isString()).isTrue()
-            val mapValueType = mapType.parameters.last()
+            val mapValueType = mapType.arguments.last()
             assertThat(mapValueType).isInstanceOf(ClassTypeItem::class.java)
             assertThat((mapValueType as ClassTypeItem).qualifiedName).isEqualTo("test.pkg.Foo")
         }
@@ -828,19 +818,19 @@
             assertThat((innerType as ClassTypeItem).qualifiedName)
                 .isEqualTo("test.pkg.Outer.Middle.Inner")
             assertThat(innerType.className).isEqualTo("Inner")
-            assertThat(innerType.parameters).isEmpty()
+            assertThat(innerType.arguments).isEmpty()
 
             val middleType = innerType.outerClassType
             assertThat(middleType).isNotNull()
             assertThat(middleType!!.qualifiedName).isEqualTo("test.pkg.Outer.Middle")
             assertThat(middleType.className).isEqualTo("Middle")
-            assertThat(middleType.parameters).isEmpty()
+            assertThat(middleType.arguments).isEmpty()
 
             val outerType = middleType.outerClassType
             assertThat(outerType).isNotNull()
             assertThat(outerType!!.qualifiedName).isEqualTo("test.pkg.Outer")
             assertThat(outerType.className).isEqualTo("Outer")
-            assertThat(outerType.parameters).isEmpty()
+            assertThat(outerType.arguments).isEmpty()
             assertThat(outerType.outerClassType).isNull()
         }
     }
@@ -865,7 +855,7 @@
                 """
                     package test.pkg
 
-                    import java.util.Map;
+                    import java.util.Map
 
                     class Test {
                         fun foo(): Map.Entry<String,String> {
@@ -958,22 +948,20 @@
             assertThat(innerType).isInstanceOf(ClassTypeItem::class.java)
             assertThat((innerType as ClassTypeItem).qualifiedName).isEqualTo("test.pkg.Outer.Inner")
             assertThat(innerType.className).isEqualTo("Inner")
-            assertThat(innerType.parameters).hasSize(1)
-            val innerTypeParameter = innerType.parameters.single()
-            assertThat(innerTypeParameter).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((innerTypeParameter as VariableTypeItem).name).isEqualTo("P2")
-            assertThat(innerTypeParameter.asTypeParameter).isEqualTo(p2)
+            assertThat(innerType.arguments).hasSize(1)
+            val innerTypeArgument = innerType.arguments.single()
+            innerTypeArgument.assertReferencesTypeParameter(p2)
+            assertThat((innerTypeArgument as VariableTypeItem).name).isEqualTo("P2")
 
             val outerType = innerType.outerClassType
             assertThat(outerType).isNotNull()
             assertThat(outerType!!.qualifiedName).isEqualTo("test.pkg.Outer")
             assertThat(outerType.className).isEqualTo("Outer")
             assertThat(outerType.outerClassType).isNull()
-            assertThat(outerType.parameters).hasSize(1)
-            val outerClassParameter = outerType.parameters.single()
-            assertThat(outerClassParameter).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((outerClassParameter as VariableTypeItem).name).isEqualTo("P1")
-            assertThat(outerClassParameter.asTypeParameter).isEqualTo(p1)
+            assertThat(outerType.arguments).hasSize(1)
+            val outerClassTypeArgument = outerType.arguments.single()
+            outerClassTypeArgument.assertReferencesTypeParameter(p1)
+            assertThat((outerClassTypeArgument as VariableTypeItem).name).isEqualTo("P1")
         }
     }
 
@@ -1024,15 +1012,13 @@
             assertThat(cacheSuperclassType).isInstanceOf(ClassTypeItem::class.java)
             assertThat((cacheSuperclassType as ClassTypeItem).qualifiedName)
                 .isEqualTo("java.util.HashMap")
-            assertThat(cacheSuperclassType.parameters).hasSize(2)
+            assertThat(cacheSuperclassType.arguments).hasSize(2)
 
-            val queryVar = cacheSuperclassType.parameters[0]
-            assertThat(queryVar).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((queryVar as VariableTypeItem).asTypeParameter).isEqualTo(queryParam)
+            val queryVar = cacheSuperclassType.arguments[0]
+            queryVar.assertReferencesTypeParameter(queryParam)
 
-            val resultVar = cacheSuperclassType.parameters[1]
-            assertThat(resultVar).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((resultVar as VariableTypeItem).asTypeParameter).isEqualTo(resultParam)
+            val resultVar = cacheSuperclassType.arguments[1]
+            resultVar.assertReferencesTypeParameter(resultParam)
 
             // Verify that the MyList interface type uses the MyList type variable
             val myList = codebase.assertClass("test.pkg.MyList")
@@ -1045,13 +1031,11 @@
 
             val myListInterfaceType = myListInterfaces.single()
             assertThat(myListInterfaceType).isInstanceOf(ClassTypeItem::class.java)
-            assertThat((myListInterfaceType as ClassTypeItem).qualifiedName)
-                .isEqualTo("java.util.List")
-            assertThat(myListInterfaceType.parameters).hasSize(1)
+            assertThat(myListInterfaceType.qualifiedName).isEqualTo("java.util.List")
+            assertThat(myListInterfaceType.arguments).hasSize(1)
 
-            val eVar = myListInterfaceType.parameters.single()
-            assertThat(eVar).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((eVar as VariableTypeItem).asTypeParameter).isEqualTo(eParam)
+            val eVar = myListInterfaceType.arguments.single()
+            eVar.assertReferencesTypeParameter(eParam)
         }
     }
 
@@ -1098,119 +1082,62 @@
             assertThat(collectionOfArrayOfStringList).isInstanceOf(ClassTypeItem::class.java)
             assertThat((collectionOfArrayOfStringList as ClassTypeItem).qualifiedName)
                 .isEqualTo("java.util.Collection")
-            assertThat(collectionOfArrayOfStringList.parameters).hasSize(1)
+            assertThat(collectionOfArrayOfStringList.arguments).hasSize(1)
 
             // java.util.List<java.lang.String>[]
-            val arrayOfStringList = collectionOfArrayOfStringList.parameters.single()
+            val arrayOfStringList = collectionOfArrayOfStringList.arguments.single()
             assertThat(arrayOfStringList).isInstanceOf(ArrayTypeItem::class.java)
 
             // java.util.List<java.lang.String>
             val stringList = (arrayOfStringList as ArrayTypeItem).componentType
             assertThat(stringList).isInstanceOf(ClassTypeItem::class.java)
             assertThat((stringList as ClassTypeItem).qualifiedName).isEqualTo("java.util.List")
-            assertThat(stringList.parameters).hasSize(1)
+            assertThat(stringList.arguments).hasSize(1)
 
             // java.lang.String
-            val string = stringList.parameters.single()
+            val string = stringList.arguments.single()
             assertThat(string.isString()).isTrue()
         }
     }
 
     @Test
-    fun `check TypeItem asClass()`() {
-        runCodebaseTest(
-            java(
-                """
-                    package test.pkg;
-
-                    import java.util.Map.Entry;
-
-                    public class Test {
-                        public int field;
-
-                        public <T extends Comparable> void method(Outer<String> a,Entry<? extends String,T> b,T c,String [] ... d){}
-                    }
-
-                    class Outer<P> {}
-                """
-            ),
-            signature(
-                """
-                    // Signature format: 2.0
-                    package test.pkg {
-                      public class Test {
-                        field public int field;
-                        method public <T extends java.lang.Comparable> void method(test.pkg.Outer<java.lang.String>,java.util.Map.Entry<? extends java.lang.String,T>,T,java.lang.String[]...);
-                      }
-                      public class Outer<P> {}
-                    }
-                """
-                    .trimIndent()
-            )
-        ) {
-            val classItem = codebase.assertClass("test.pkg.Test")
-            val methodItem1 = classItem.methods()[0]
-
-            val fieldTypeClassItem = classItem.assertField("field").type().asClass()
-            val parameterTypeClassItem1 = methodItem1.parameters()[0].type().asClass()
-            val parameterTypeClassItem2 = methodItem1.parameters()[1].type().asClass()
-            val parameterTypeClassItem3 = methodItem1.parameters()[2].type().asClass()
-            val parameterTypeClassItem4 = methodItem1.parameters()[3].type().asClass()
-
-            val outerClassItem = codebase.assertClass("test.pkg.Outer")
-            val stringClassItem = codebase.assertClass("java.lang.String")
-            val entryClassItem = codebase.assertClass("java.util.Map.Entry")
-            val comparableClassItem = codebase.assertClass("java.lang.Comparable")
-
-            assertThat(fieldTypeClassItem).isNull()
-            assertThat(parameterTypeClassItem1).isEqualTo(outerClassItem)
-            assertThat(parameterTypeClassItem2).isEqualTo(entryClassItem)
-            assertThat(parameterTypeClassItem3).isEqualTo(comparableClassItem)
-            assertThat(parameterTypeClassItem4).isEqualTo(stringClassItem)
-        }
-    }
-
-    @Test
     fun `Test Kotlin collection removeAll parameter type`() {
         runCodebaseTest(
             kotlin(
                 """
                     package test.pkg
-                    abstract class Foo<E> : MutableCollection<E> {
-                        override fun addAll(elements: Collection<E>): Boolean = true
-                        override fun removeAll(elements: Collection<E>): Boolean = true
+                    abstract class Foo<Z> : MutableCollection<Z> {
+                        override fun addAll(elements: Collection<Z>): Boolean = true
+                        override fun removeAll(elements: Collection<Z>): Boolean = true
                     }
                 """
-                    .trimIndent()
             )
         ) {
             val fooClass = codebase.assertClass("test.pkg.Foo")
             val typeParam = fooClass.typeParameterList().typeParameters().single()
 
             // Defined in `java.util.Collection` as `addAll(Collection<? extends E> c)`
-            val addAllParam =
-                fooClass.methods().single { it.name() == "addAll" }.parameters().single().type()
+            val addAllMethod = fooClass.methods().single { it.name() == "addAll" }
+            val addAllParam = addAllMethod.parameters().single().type()
             assertThat(addAllParam).isInstanceOf(ClassTypeItem::class.java)
             assertThat((addAllParam as ClassTypeItem).qualifiedName)
                 .isEqualTo("java.util.Collection")
-            assertThat(addAllParam.parameters).hasSize(1)
-            val addAllWildcard = addAllParam.parameters.single()
+            assertThat(addAllParam.arguments).hasSize(1)
+            val addAllWildcard = addAllParam.arguments.single()
             assertThat(addAllWildcard).isInstanceOf(WildcardTypeItem::class.java)
-            val allAllE = (addAllWildcard as WildcardTypeItem).extendsBound
-            assertThat(allAllE).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((allAllE as VariableTypeItem).asTypeParameter).isEqualTo(typeParam)
+            val allAllZ = (addAllWildcard as WildcardTypeItem).extendsBound
+            allAllZ!!.assertReferencesTypeParameter(typeParam)
 
             // Defined in `java.util.Collection` as `removeAll(Collection<?> c)`
             // Appears in psi as a `PsiImmediateClassType` with no parameters
-            val removeAllParam =
-                fooClass.methods().single { it.name() == "removeAll" }.parameters().single().type()
+            val removeAllMethod = fooClass.methods().single { it.name() == "removeAll" }
+            val removeAllParam = removeAllMethod.parameters().single().type()
             assertThat(removeAllParam).isInstanceOf(ClassTypeItem::class.java)
             assertThat((removeAllParam as ClassTypeItem).qualifiedName)
                 .isEqualTo("java.util.Collection")
-            assertThat(removeAllParam.parameters).hasSize(1)
-            val removeAllE = removeAllParam.parameters.single()
-            assertThat(removeAllE).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((removeAllE as VariableTypeItem).asTypeParameter).isEqualTo(typeParam)
+            assertThat(removeAllParam.arguments).hasSize(1)
+            val removeAllZ = removeAllParam.arguments.single()
+            removeAllZ.assertReferencesTypeParameter(typeParam)
         }
     }
 
@@ -1286,26 +1213,26 @@
             val mVar = parent.assertMethod("getM", "").returnType()
             val xVar = mVar.convertType(child, parent)
             assertThat(xVar.toTypeString()).isEqualTo("X")
-            assertThat((xVar as VariableTypeItem).asTypeParameter).isEqualTo(x)
+            xVar.assertReferencesTypeParameter(x)
 
             val nArray = parent.assertMethod("getNArray", "").returnType()
             val yArray = nArray.convertType(child, parent)
             assertThat(yArray.toTypeString()).isEqualTo("Y[]")
             assertThat((yArray as ArrayTypeItem).isVarargs).isFalse()
-            assertThat((yArray.componentType as VariableTypeItem).asTypeParameter).isEqualTo(y)
+            yArray.componentType.assertReferencesTypeParameter(y)
 
             val mList = parent.assertMethod("getMList", "").returnType()
             val xList = mList.convertType(child, parent)
             assertThat(xList.toTypeString()).isEqualTo("java.util.List<X>")
             assertThat((xList as ClassTypeItem).qualifiedName).isEqualTo("java.util.List")
-            assertThat((xList.parameters.single() as VariableTypeItem).asTypeParameter).isEqualTo(x)
+            xList.arguments.single().assertReferencesTypeParameter(x)
 
             val mToNMap = parent.assertMethod("getMap", "").returnType()
             val xToYMap = mToNMap.convertType(child, parent)
             assertThat(xToYMap.toTypeString()).isEqualTo("java.util.Map<X,Y>")
             assertThat((xToYMap as ClassTypeItem).qualifiedName).isEqualTo("java.util.Map")
-            assertThat((xToYMap.parameters[0] as VariableTypeItem).asTypeParameter).isEqualTo(x)
-            assertThat((xToYMap.parameters[1] as VariableTypeItem).asTypeParameter).isEqualTo(y)
+            xToYMap.arguments[0].assertReferencesTypeParameter(x)
+            xToYMap.arguments[1].assertReferencesTypeParameter(y)
 
             val wildcards = parent.assertMethod("getWildcards", "").returnType()
             val convertedWildcards = wildcards.convertType(child, parent)
@@ -1313,12 +1240,12 @@
                 .isEqualTo("test.pkg.Parent<? extends X,? super Y>")
             assertThat((convertedWildcards as ClassTypeItem).qualifiedName)
                 .isEqualTo("test.pkg.Parent")
-            assertThat(convertedWildcards.parameters).hasSize(2)
+            assertThat(convertedWildcards.arguments).hasSize(2)
 
-            val extendsX = convertedWildcards.parameters[0] as WildcardTypeItem
-            assertThat((extendsX.extendsBound as VariableTypeItem).asTypeParameter).isEqualTo(x)
-            val superN = convertedWildcards.parameters[1] as WildcardTypeItem
-            assertThat((superN.superBound as VariableTypeItem).asTypeParameter).isEqualTo(y)
+            val extendsX = convertedWildcards.arguments[0] as WildcardTypeItem
+            extendsX.extendsBound!!.assertReferencesTypeParameter(x)
+            val superN = convertedWildcards.arguments[1] as WildcardTypeItem
+            superN.superBound!!.assertReferencesTypeParameter(y)
         }
     }
 
@@ -1329,16 +1256,26 @@
                 """
                     package test.pkg;
                     import java.util.List;
-                    public class Foo<T> {
-                        public int intField;
-                        public char charField;
-                        public String stringField;
-                        public T tField;
-                        public String[] stringArrayField;
-                        public List<String> listStringField;
-                        public List<List<String>> listListStringField;
-                        public Foo<? extends String> fooExtendsStringField;
-                        public Foo<? super String> fooSuperStringField;
+                    public class Foo<T, X> {
+                      public Number numberType;
+
+                      public int primitiveType;
+                      public int primitiveTypeAfterMatchingConversion;
+
+                      public T variableType;
+                      public Number variableTypeAfterMatchingConversion;
+
+                      public T[] arrayType;
+                      public Number[] arrayTypeAfterMatchingConversion;
+
+                      public Foo<T, String> classType;
+                      public Foo<Number, String> classTypeAfterMatchingConversion;
+
+                      public Foo<? extends T, String> wildcardExtendsType;
+                      public Foo<? extends Number, String> wildcardExtendsTypeAfterMatchingConversion;
+
+                      public Foo<? super T, String> wildcardSuperType;
+                      public Foo<? super Number, String> wildcardSuperTypeAfterMatchingConversion;
                     }
                 """
                     .trimIndent()
@@ -1346,16 +1283,26 @@
             kotlin(
                 """
                     package test.pkg
-                    class Foo<T> {
-                        @JvmField val intField: Int
-                        @JvmField val charField: Char
-                        @JvmField val stringField: String
-                        @JvmField val tField: T
-                        @JvmField val stringArrayField: Array<String>
-                        @JvmField val listStringField: List<String>
-                        @JvmField val listListStringField: List<List<String>>
-                        @JvmField val fooExtendsStringField: Foo<out String>
-                        @JvmField  val fooSuperStringField: Foo<in String>
+                    class Foo<T, X> {
+                        @JvmField val numberType: Number
+
+                        @JvmField val primitiveType: Int
+                        @JvmField val primitiveTypeAfterMatchingConversion: Int
+
+                        @JvmField val variableType: T
+                        @JvmField val variableTypeAfterMatchingConversion: Number
+
+                        @JvmField val arrayType: Array<T>
+                        @JvmField val arrayTypeAfterMatchingConversion: Array<Number>
+
+                        @JvmField val classType: Foo<T, String>
+                        @JvmField val classTypeAfterMatchingConversion: Foo<Number, String>
+
+                        @JvmField val wildcardExtendsType: Foo<out T, String>
+                        @JvmField val wildcardExtendsTypeAfterMatchingConversion: Foo<out Number, String>
+
+                        @JvmField val wildcardSuperType: Foo<in T, String>
+                        @JvmField val wildcardSuperTypeAfterMatchingConversion: Foo<in Number, String>
                     }
                 """
                     .trimIndent()
@@ -1364,16 +1311,26 @@
                 """
                     // Signature format: 5.0
                     package test.pkg {
-                      public class Foo {
-                        field public int intField;
-                        field public char charField;
-                        field public String stringField;
-                        field public T tField;
-                        field public String[] stringArrayField;
-                        field public java.util.List<java.lang.String> listStringField;
-                        field public java.util.List<java.util.List<java.lang.String>> listListStringField;
-                        field public test.pkg.Foo<? extends java.lang.String> fooExtendsStringField;
-                        field public test.pkg.Foo<? super java.lang.String> fooSuperStringField;
+                      public class Foo<T, X> {
+                        field public Number numberType;
+
+                        field public int primitiveType;
+                        field public int primitiveTypeAfterMatchingConversion;
+
+                        field public T variableType;
+                        field public Number variableTypeAfterMatchingConversion;
+
+                        field public T[] arrayType;
+                        field public Number[] arrayTypeAfterMatchingConversion;
+
+                        field public test.pkg.Foo<T, String> classType;
+                        field public test.pkg.Foo<Number, String> classTypeAfterMatchingConversion;
+
+                        field public test.pkg.Foo<? extends T, String> wildcardExtendsType;
+                        field public test.pkg.Foo<? extends Number, String> wildcardExtendsTypeAfterMatchingConversion;
+
+                        field public test.pkg.Foo<? super T, String> wildcardSuperType;
+                        field public test.pkg.Foo<? super Number, String> wildcardSuperTypeAfterMatchingConversion;
                       }
                     }
                 """
@@ -1381,72 +1338,67 @@
             )
         ) {
             val fooClass = codebase.assertClass("test.pkg.Foo")
+            val t = fooClass.typeParameterList().typeParameters().single { it.name() == "T" }
+            val x = fooClass.typeParameterList().typeParameters().single { it.name() == "X" }
+            val numberType = fooClass.assertField("numberType").type() as ReferenceTypeItem
 
-            val int = fooClass.fields().single { it.name() == "intField" }.type()
-            val char = fooClass.fields().single { it.name() == "charField" }.type()
-            val string = fooClass.fields().single { it.name() == "stringField" }.type()
-            val t = fooClass.fields().single { it.name() == "tField" }.type()
-            val stringArray = fooClass.fields().single { it.name() == "stringArrayField" }.type()
-            val listString = fooClass.fields().single { it.name() == "listStringField" }.type()
-            val listListString =
-                fooClass.fields().single { it.name() == "listListStringField" }.type()
-            val fooExtendsString =
-                fooClass.fields().single { it.name() == "fooExtendsStringField" }.type()
-            val fooSuperString =
-                fooClass.fields().single { it.name() == "fooSuperStringField" }.type()
+            val matchingBindings = mapOf(t to numberType)
+            val nonMatchingBindings = mapOf(x to numberType)
 
-            // Converting primitive when it is in map and when it isn't
-            assertThat(int.convertType(mapOf(int to string))).isEqualTo(string)
-            assertThat(int.convertType(mapOf(char to string))).isEqualTo(int)
+            val afterMatchingConversionSuffix = "AfterMatchingConversion"
+            val fieldsToCheck =
+                fooClass.fields().filter {
+                    it.name() != "numberType" && !it.name().endsWith(afterMatchingConversionSuffix)
+                }
 
-            // Converting class when it is in map and when it isn't
-            assertThat(string.convertType(mapOf(string to int))).isEqualTo(int)
-            assertThat(string.convertType(mapOf(string to stringArray))).isEqualTo(stringArray)
-            assertThat(string.convertType(mapOf(char to string))).isEqualTo(string)
+            for (fieldItem in fieldsToCheck) {
+                val fieldType = fieldItem.type()
 
-            // Converting variable when it is in map and when it isn't
-            assertThat(t.convertType(mapOf(t to int))).isEqualTo(int)
-            assertThat(t.convertType(mapOf(t to fooExtendsString))).isEqualTo(fooExtendsString)
-            assertThat(t.convertType(mapOf(char to string))).isEqualTo(t)
+                val fieldName = fieldItem.name()
+                val expectedMatchedFieldType =
+                    fooClass.assertField(fieldName + afterMatchingConversionSuffix).type()
 
-            // Converting array when it is in map, when it isn't, and when component is in map
-            assertThat(stringArray.convertType(mapOf(stringArray to int))).isEqualTo(int)
-            assertThat(stringArray.convertType(mapOf(char to string))).isEqualTo(stringArray)
-            val convertedArray = stringArray.convertType(mapOf(string to int))
-            assertThat(convertedArray).isInstanceOf(ArrayTypeItem::class.java)
-            assertThat((convertedArray as ArrayTypeItem).componentType).isEqualTo(int)
+                assertWithMessage("conversion that matches $fieldName")
+                    .that(fieldType.convertType(matchingBindings))
+                    .isEqualTo(expectedMatchedFieldType)
 
-            // Converting class parameters
-            val convertedList = listString.convertType(mapOf(string to stringArray))
-            assertThat(convertedList).isInstanceOf(ClassTypeItem::class.java)
-            assertThat((convertedList as ClassTypeItem).qualifiedName).isEqualTo("java.util.List")
-            assertThat(convertedList.parameters.single()).isEqualTo(stringArray)
+                // Expect no change if it does not match.
+                assertWithMessage("conversion that does not match $fieldName")
+                    .that(fieldType.convertType(nonMatchingBindings))
+                    .isEqualTo(fieldType)
+            }
+        }
+    }
 
-            val convertedListList = listListString.convertType(mapOf(string to stringArray))
-            assertThat(convertedListList).isInstanceOf(ClassTypeItem::class.java)
-            assertThat((convertedListList as ClassTypeItem).qualifiedName)
-                .isEqualTo("java.util.List")
-            assertThat(convertedListList.parameters.single()).isEqualTo(convertedList)
+    @Test
+    fun `Test hasTypeArguments`() {
+        runCodebaseTest(
+            java(
+                """
+                    package test.pkg;
+                    public abstract class Foo implements Comparable<String> {}
+                """
+            ),
+            kotlin(
+                """
+                    package test.pkg
+                    abstract class Foo: Comparable<String>
+                """
+            ),
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                      public abstract class Foo implements Comparable<String> {}
+                    }
+                """
+            ),
+        ) {
+            val classType = codebase.assertClass("test.pkg.Foo").type()
+            assertThat(classType.hasTypeArguments()).isFalse()
 
-            // Converting extends type
-            val convertedExtendsType = fooExtendsString.convertType(mapOf(string to int))
-            assertThat(convertedExtendsType).isInstanceOf(ClassTypeItem::class.java)
-            assertThat((convertedExtendsType as ClassTypeItem).qualifiedName)
-                .isEqualTo("test.pkg.Foo")
-            val extendsType = convertedExtendsType.parameters.single()
-            assertThat(extendsType).isInstanceOf(WildcardTypeItem::class.java)
-            assertThat((extendsType as WildcardTypeItem).extendsBound).isEqualTo(int)
-            assertThat(fooExtendsString.convertType(mapOf(char to int))).isEqualTo(fooExtendsString)
-
-            // Converting super type
-            val convertedSuperType = fooSuperString.convertType(mapOf(string to int))
-            assertThat(convertedSuperType).isInstanceOf(ClassTypeItem::class.java)
-            assertThat((convertedSuperType as ClassTypeItem).qualifiedName)
-                .isEqualTo("test.pkg.Foo")
-            val superType = convertedSuperType.parameters.single()
-            assertThat(superType).isInstanceOf(WildcardTypeItem::class.java)
-            assertThat((superType as WildcardTypeItem).superBound).isEqualTo(int)
-            assertThat(fooSuperString.convertType(mapOf(char to int))).isEqualTo(fooSuperString)
+            val interfaceType = codebase.assertClass("test.pkg.Foo").interfaceTypes().single()
+            assertThat(interfaceType.hasTypeArguments()).isTrue()
         }
     }
 }
diff --git a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/typeitem/CommonTypeModifiersTest.kt b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/typeitem/CommonTypeModifiersTest.kt
index 2b46bcf..1e2b974 100644
--- a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/typeitem/CommonTypeModifiersTest.kt
+++ b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/typeitem/CommonTypeModifiersTest.kt
@@ -174,8 +174,7 @@
             val variableMethod = methods[2]
             val variable = variableMethod.returnType()
             val typeParameter = variableMethod.typeParameterList().typeParameters().single()
-            assertThat(variable).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((variable as VariableTypeItem).asTypeParameter).isEqualTo(typeParameter)
+            variable.assertReferencesTypeParameter(typeParameter)
             assertThat(variable.annotationNames()).containsExactly("test.pkg.A")
             assertThat(variableMethod.annotationNames()).isEmpty()
         }
@@ -246,8 +245,7 @@
             val variableMethod = methods[2]
             val variable = variableMethod.returnType()
             val typeParameter = variableMethod.typeParameterList().typeParameters().single()
-            assertThat(variable).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((variable as VariableTypeItem).asTypeParameter).isEqualTo(typeParameter)
+            variable.assertReferencesTypeParameter(typeParameter)
             assertThat(variable.annotationNames()).containsExactly("test.pkg.A")
             assertThat(variableMethod.annotationNames()).containsExactly("test.pkg.A")
         }
@@ -290,8 +288,7 @@
             val variableMethod = methods[2]
             val variable = variableMethod.returnType()
             val typeParameter = variableMethod.typeParameterList().typeParameters().single()
-            assertThat(variable).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((variable as VariableTypeItem).asTypeParameter).isEqualTo(typeParameter)
+            variable.assertReferencesTypeParameter(typeParameter)
             assertThat(variable.annotationNames()).isEmpty()
             assertThat(variableMethod.annotationNames()).containsExactly("test.pkg.A")
         }
@@ -399,15 +396,15 @@
             val mapType = method.returnType()
             assertThat(mapType).isInstanceOf(ClassTypeItem::class.java)
             assertThat(mapType.annotationNames()).containsExactly("test.pkg.A")
-            assertThat((mapType as ClassTypeItem).parameters).hasSize(2)
+            assertThat((mapType as ClassTypeItem).arguments).hasSize(2)
 
             // [email protected] @test.pkg.C String
-            val string1 = mapType.parameters[0]
+            val string1 = mapType.arguments[0]
             assertThat(string1.isString()).isTrue()
             assertThat(string1.annotationNames()).containsExactly("test.pkg.B", "test.pkg.C")
 
             // [email protected] String
-            val string2 = mapType.parameters[1]
+            val string2 = mapType.arguments[1]
             assertThat(string2.isString()).isTrue()
             assertThat(string2.annotationNames()).containsExactly("test.pkg.D")
         }
@@ -480,9 +477,7 @@
             val arrayType = method.returnType()
             assertThat(arrayType).isInstanceOf(ArrayTypeItem::class.java)
             val componentType = (arrayType as ArrayTypeItem).componentType
-            assertThat(componentType).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((componentType as VariableTypeItem).asTypeParameter)
-                .isEqualTo(methodTypeParam)
+            componentType.assertReferencesTypeParameter(methodTypeParam)
             assertThat(componentType.annotationNames()).containsExactly("test.pkg.A")
         }
     }
@@ -620,27 +615,25 @@
             val innerType = method.returnType()
             assertThat(innerType).isInstanceOf(ClassTypeItem::class.java)
             assertThat((innerType as ClassTypeItem).qualifiedName).isEqualTo("test.pkg.Outer.Inner")
-            assertThat(innerType.parameters).hasSize(1)
+            assertThat(innerType.arguments).hasSize(1)
             assertThat(innerType.annotationNames()).containsExactly("test.pkg.C")
 
-            val innerTypeParameter = innerType.parameters.single()
-            assertThat(innerTypeParameter).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((innerTypeParameter as VariableTypeItem).name).isEqualTo("P2")
-            assertThat(innerTypeParameter.asTypeParameter).isEqualTo(p2)
-            assertThat(innerTypeParameter.annotationNames()).containsExactly("test.pkg.D")
+            val innerTypeArgument = innerType.arguments.single()
+            innerTypeArgument.assertReferencesTypeParameter(p2)
+            assertThat((innerTypeArgument as VariableTypeItem).name).isEqualTo("P2")
+            assertThat(innerTypeArgument.annotationNames()).containsExactly("test.pkg.D")
 
             val outerType = innerType.outerClassType
             assertThat(outerType).isNotNull()
             assertThat(outerType!!.qualifiedName).isEqualTo("test.pkg.Outer")
             assertThat(outerType.outerClassType).isNull()
-            assertThat(outerType.parameters).hasSize(1)
+            assertThat(outerType.arguments).hasSize(1)
             assertThat(outerType.annotationNames()).containsExactly("test.pkg.A")
 
-            val outerClassParameter = outerType.parameters.single()
-            assertThat(outerClassParameter).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((outerClassParameter as VariableTypeItem).name).isEqualTo("P1")
-            assertThat(outerClassParameter.asTypeParameter).isEqualTo(p1)
-            assertThat(outerClassParameter.annotationNames()).containsExactly("test.pkg.B")
+            val outerClassArgument = outerType.arguments.single()
+            outerClassArgument.assertReferencesTypeParameter(p1)
+            assertThat((outerClassArgument as VariableTypeItem).name).isEqualTo("P1")
+            assertThat(outerClassArgument.annotationNames()).containsExactly("test.pkg.B")
         }
     }
 
@@ -670,14 +663,14 @@
 
             val bar = interfaces[0]
             assertThat(bar).isInstanceOf(ClassTypeItem::class.java)
-            assertThat((bar as ClassTypeItem).qualifiedName).isEqualTo("test.pkg.Bar")
+            assertThat(bar.qualifiedName).isEqualTo("test.pkg.Bar")
             val annotations = bar.modifiers.annotations()
             assertThat(annotations).hasSize(1)
             assertThat(annotations.single().qualifiedName).isEqualTo("test.pkg.A")
 
             val baz = interfaces[1]
             assertThat(baz).isInstanceOf(ClassTypeItem::class.java)
-            assertThat((baz as ClassTypeItem).qualifiedName).isEqualTo("test.pkg.Baz")
+            assertThat(baz.qualifiedName).isEqualTo("test.pkg.Baz")
         }
     }
 
@@ -744,22 +737,22 @@
 
             val bar = interfaces[0]
             assertThat(bar).isInstanceOf(ClassTypeItem::class.java)
-            assertThat((bar as ClassTypeItem).qualifiedName).isEqualTo("test.pkg.Bar")
+            assertThat(bar.qualifiedName).isEqualTo("test.pkg.Bar")
             assertThat(bar.annotationNames()).containsExactly("test.pkg.A")
 
             val baz = interfaces[1]
             assertThat(baz).isInstanceOf(ClassTypeItem::class.java)
-            assertThat((baz as ClassTypeItem).qualifiedName).isEqualTo("test.pkg.Baz")
-            assertThat(baz.parameters).hasSize(1)
+            assertThat(baz.qualifiedName).isEqualTo("test.pkg.Baz")
+            assertThat(baz.arguments).hasSize(1)
             assertThat(baz.annotationNames()).containsExactly("test.pkg.B")
 
-            val bazParam = baz.parameters.single()
-            assertThat(bazParam.isString()).isTrue()
-            assertThat(bazParam.annotationNames()).containsExactly("test.pkg.C")
+            val bazTypeArgument = baz.arguments.single()
+            assertThat(bazTypeArgument.isString()).isTrue()
+            assertThat(bazTypeArgument.annotationNames()).containsExactly("test.pkg.C")
 
             val biz = interfaces[2]
             assertThat(biz).isInstanceOf(ClassTypeItem::class.java)
-            assertThat((biz as ClassTypeItem).qualifiedName).isEqualTo("test.pkg.Biz")
+            assertThat(biz.qualifiedName).isEqualTo("test.pkg.Biz")
             assertThat(biz.annotationNames()).isEmpty()
         }
     }
@@ -847,10 +840,10 @@
 
             val interfaces = fooClass.interfaceTypes()
             val bazInterface = interfaces[0]
-            assertThat((bazInterface as ClassTypeItem).qualifiedName).isEqualTo("test.pkg.Baz")
+            assertThat(bazInterface.qualifiedName).isEqualTo("test.pkg.Baz")
             testModifiers(bazInterface.modifiers)
             val bizInterface = interfaces[1]
-            assertThat((bizInterface as ClassTypeItem).qualifiedName).isEqualTo("test.pkg.Biz")
+            assertThat(bizInterface.qualifiedName).isEqualTo("test.pkg.Biz")
             testModifiers(bizInterface.modifiers)
 
             val fooMethod = fooClass.methods().single()
@@ -859,13 +852,13 @@
             val typeVarArray = fooMethod.parameters().single().type()
             testModifiers(typeVarArray.modifiers)
             val typeVar = (typeVarArray as ArrayTypeItem).componentType
-            assertThat((typeVar as VariableTypeItem).asTypeParameter).isEqualTo(typeParam)
+            typeVar.assertReferencesTypeParameter(typeParam)
             testModifiers(typeVar.modifiers)
 
             val stringList = fooMethod.returnType()
             assertThat((stringList as ClassTypeItem).qualifiedName).isEqualTo("java.util.List")
             testModifiers(stringList.modifiers)
-            val string = stringList.parameters.single()
+            val string = stringList.arguments.single()
             assertThat(string.isString()).isTrue()
             testModifiers(string.modifiers)
         }
@@ -1411,24 +1404,24 @@
                 val nullableListPlatformString =
                     fooClass.assertMethod("nullableListPlatformString", "").returnType()
                 assertNullable(nullableListPlatformString, annotations)
-                assertPlatform((nullableListPlatformString as ClassTypeItem).parameters.single())
+                assertPlatform((nullableListPlatformString as ClassTypeItem).arguments.single())
             }
 
             val nonNullListNullableString =
                 fooClass.assertMethod("nonNullListNullableString", "").returnType()
             assertNonNull(nonNullListNullableString, annotations)
             assertNullable(
-                (nonNullListNullableString as ClassTypeItem).parameters.single(),
+                (nonNullListNullableString as ClassTypeItem).arguments.single(),
                 annotations
             )
 
             val nullableMap = fooClass.assertMethod("nullableMap", "").returnType()
             assertNullable(nullableMap, annotations)
-            val mapParams = (nullableMap as ClassTypeItem).parameters
+            val mapTypeArguments = (nullableMap as ClassTypeItem).arguments
             // Non-null Integer
-            assertNonNull(mapParams[0], annotations)
+            assertNonNull(mapTypeArguments[0], annotations)
             // Nullable String
-            assertNullable(mapParams[1], annotations)
+            assertNullable(mapTypeArguments[1], annotations)
         }
     }
 
@@ -1491,11 +1484,11 @@
         ) { codebase, annotations ->
             val innerClass = codebase.assertClass("test.pkg.Foo").methods().single().returnType()
             assertNullable(innerClass, annotations)
-            assertNonNull((innerClass as ClassTypeItem).parameters.single(), annotations)
+            assertNonNull((innerClass as ClassTypeItem).arguments.single(), annotations)
             val outerClass = innerClass.outerClassType!!
             // Outer class types can't be null and don't need to be annotated.
             assertNonNull(outerClass, expectAnnotation = false)
-            assertNullable(outerClass.parameters.single(), annotations)
+            assertNullable(outerClass.arguments.single(), annotations)
         }
     }
 
@@ -1563,21 +1556,21 @@
 
             val extendsBoundFoo = fooClass.assertMethod("extendsBound", "").returnType()
             assertNonNull(extendsBoundFoo, annotations)
-            val extendsBound = (extendsBoundFoo as ClassTypeItem).parameters.single()
+            val extendsBound = (extendsBoundFoo as ClassTypeItem).arguments.single()
             assertUndefinedNullness(extendsBound)
             val nullableString = (extendsBound as WildcardTypeItem).extendsBound
             assertNullable(nullableString!!, annotations)
 
             val superBoundFoo = fooClass.assertMethod("superBound", "").returnType()
             assertNonNull(superBoundFoo, annotations)
-            val superBound = (superBoundFoo as ClassTypeItem).parameters.single()
+            val superBound = (superBoundFoo as ClassTypeItem).arguments.single()
             assertUndefinedNullness(superBound)
             val nonNullString = (superBound as WildcardTypeItem).superBound
             assertNonNull(nonNullString!!, annotations)
 
             val unboundedFoo = fooClass.assertMethod("unbounded", "").returnType()
             assertNonNull(unboundedFoo, annotations)
-            val unbounded = (unboundedFoo as ClassTypeItem).parameters.single()
+            val unbounded = (unboundedFoo as ClassTypeItem).arguments.single()
             assertUndefinedNullness(unbounded)
         }
     }
@@ -1912,7 +1905,7 @@
             // method public static getEntries(): kotlin.enums.EnumEntries<test.pkg.Foo>;
             val enumEntries = fooEnum.assertMethod("getEntries", "").returnType()
             assertNonNull(enumEntries, expectAnnotation = false)
-            val enumEntry = (enumEntries as ClassTypeItem).parameters.single()
+            val enumEntry = (enumEntries as ClassTypeItem).arguments.single()
             assertNonNull(enumEntry, expectAnnotation = false)
         }
     }
@@ -1956,30 +1949,30 @@
             // () -> String
             val noParamToString = fooClass.assertMethod("noParamToString", "").returnType()
             assertNonNull(noParamToString, expectAnnotation = false)
-            assertThat((noParamToString as ClassTypeItem).parameters).hasSize(1)
-            assertNonNull(noParamToString.parameters.single(), expectAnnotation = false)
+            assertThat((noParamToString as ClassTypeItem).arguments).hasSize(1)
+            assertNonNull(noParamToString.arguments.single(), expectAnnotation = false)
 
             // (String?) -> String
             val oneParamToString = fooClass.assertMethod("oneParamToString", "").returnType()
             assertNonNull(oneParamToString, expectAnnotation = false)
-            assertThat((oneParamToString as ClassTypeItem).parameters).hasSize(2)
-            assertNullable(oneParamToString.parameters[0], expectAnnotation = false)
-            assertNonNull(oneParamToString.parameters[1], expectAnnotation = false)
+            assertThat((oneParamToString as ClassTypeItem).arguments).hasSize(2)
+            assertNullable(oneParamToString.arguments[0], expectAnnotation = false)
+            assertNonNull(oneParamToString.arguments[1], expectAnnotation = false)
 
             // (String, Int?) -> String?
             val twoParamToString = fooClass.assertMethod("twoParamToString", "").returnType()
             assertNonNull(twoParamToString, expectAnnotation = false)
-            assertThat((twoParamToString as ClassTypeItem).parameters).hasSize(3)
-            assertNonNull(twoParamToString.parameters[0], expectAnnotation = false)
-            assertNullable(twoParamToString.parameters[1], expectAnnotation = false)
-            assertNullable(twoParamToString.parameters[2], expectAnnotation = false)
+            assertThat((twoParamToString as ClassTypeItem).arguments).hasSize(3)
+            assertNonNull(twoParamToString.arguments[0], expectAnnotation = false)
+            assertNullable(twoParamToString.arguments[1], expectAnnotation = false)
+            assertNullable(twoParamToString.arguments[2], expectAnnotation = false)
 
             // (String) -> Unit
             val oneParamToUnit = fooClass.assertMethod("oneParamToUnit", "").returnType()
             assertNonNull(oneParamToUnit, expectAnnotation = false)
-            assertThat((oneParamToUnit as ClassTypeItem).parameters).hasSize(2)
-            assertNonNull(oneParamToUnit.parameters[0], expectAnnotation = false)
-            assertNonNull(oneParamToUnit.parameters[1], expectAnnotation = false)
+            assertThat((oneParamToUnit as ClassTypeItem).arguments).hasSize(2)
+            assertNonNull(oneParamToUnit.arguments[0], expectAnnotation = false)
+            assertNonNull(oneParamToUnit.arguments[1], expectAnnotation = false)
         }
     }
 
@@ -2036,16 +2029,13 @@
             assertNonNull(propType, expectAnnotation = false)
             assertNonNull(getterType, expectAnnotation = false)
             assertNonNull(setterType, expectAnnotation = false)
+            assertNullable((propType as ClassTypeItem).arguments.single(), expectAnnotation = false)
             assertNullable(
-                (propType as ClassTypeItem).parameters.single(),
+                (getterType as ClassTypeItem).arguments.single(),
                 expectAnnotation = false
             )
             assertNullable(
-                (getterType as ClassTypeItem).parameters.single(),
-                expectAnnotation = false
-            )
-            assertNullable(
-                (setterType as ClassTypeItem).parameters.single(),
+                (setterType as ClassTypeItem).arguments.single(),
                 expectAnnotation = false
             )
         }
@@ -2067,13 +2057,13 @@
             val extensionFunctionType =
                 codebase.assertClass("test.pkg.Foo").methods().single().returnType()
             assertNonNull(extensionFunctionType, expectAnnotation = false)
-            val receiverType = (extensionFunctionType as ClassTypeItem).parameters[0]
+            val receiverType = (extensionFunctionType as ClassTypeItem).arguments[0]
             assertNullable(receiverType, expectAnnotation = false)
-            val parameter1Type = extensionFunctionType.parameters[1]
-            assertNonNull(parameter1Type, expectAnnotation = false)
-            val parameter2Type = extensionFunctionType.parameters[2]
-            assertNullable(parameter2Type, expectAnnotation = false)
-            val returnType = extensionFunctionType.parameters[3]
+            val typeArgument1 = extensionFunctionType.arguments[1]
+            assertNonNull(typeArgument1, expectAnnotation = false)
+            val typeArgument2 = extensionFunctionType.arguments[2]
+            assertNullable(typeArgument2, expectAnnotation = false)
+            val returnType = extensionFunctionType.arguments[3]
             assertNonNull(returnType, expectAnnotation = false)
         }
     }
@@ -2094,10 +2084,120 @@
         ) {
             val functionType = codebase.assertClass("test.pkg.Foo").methods().single().returnType()
             assertNullable(functionType, expectAnnotation = false)
-            val parameterType = (functionType as ClassTypeItem).parameters[0]
-            assertNonNull(parameterType, expectAnnotation = false)
-            val returnType = functionType.parameters[1]
+            val typeArgument = (functionType as ClassTypeItem).arguments[0]
+            assertNonNull(typeArgument, expectAnnotation = false)
+            val returnType = functionType.arguments[1]
             assertNullable(returnType, expectAnnotation = false)
         }
     }
+
+    @Test
+    fun `Test nullability of super class type`() {
+        runCodebaseTest(
+            java(
+                """
+                    package test.pkg;
+                    public class Foo extends Number {}
+                """
+            ),
+            kotlin(
+                """
+                    package test.pkg
+                    class Foo: Number {
+                    }
+                """
+            ),
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                      public class Foo extends Number {
+                      }
+                    }
+                """
+            ),
+        ) {
+            val superClassType = codebase.assertClass("test.pkg.Foo").superClassType()!!
+            assertNonNull(superClassType, expectAnnotation = false)
+        }
+    }
+
+    @Test
+    fun `Test nullability of super interface type`() {
+        runCodebaseTest(
+            java(
+                """
+                    package test.pkg;
+                    import java.util.Map;
+                    public abstract class Foo implements Map.Entry<String, String> {}
+                """
+            ),
+            kotlin(
+                """
+                    package test.pkg
+                    import java.util.Map
+                    abstract class Foo: Map.Entry<String, String> {
+                    }
+                """
+            ),
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                      public abstract class Foo implements java.util.Map.Entry<java.lang.String, java.lang.String> {
+                      }
+                    }
+                """
+            ),
+        ) {
+            val superInterfaceType = codebase.assertClass("test.pkg.Foo").interfaceTypes().single()
+
+            // The outer class type must be non-null.
+            val outerClassType = superInterfaceType.outerClassType!!
+            assertNonNull(outerClassType, expectAnnotation = false)
+
+            // As must the nested class.
+            assertNonNull(superInterfaceType, expectAnnotation = false)
+        }
+    }
+
+    @Test
+    fun `Test nullability of generic super class and interface type`() {
+        runCodebaseTest(
+            java(
+                """
+                    package test.pkg;
+                    import java.util.List;
+                    public abstract class Foo<E> extends Number implements List<E> {}
+                """
+            ),
+            kotlin(
+                """
+                    package test.pkg
+                    import java.util.List
+                    abstract class Foo<E>: List<E> {
+                    }
+                """
+            ),
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                      public abstract class Foo<E> extends Number implements java.util.List<E> {
+                      }
+                    }
+                """
+            ),
+        ) {
+            val fooClass = codebase.assertClass("test.pkg.Foo")
+
+            // The super class type must be non-null.
+            val superClassType = codebase.assertClass("test.pkg.Foo").superClassType()!!
+            assertNonNull(superClassType, expectAnnotation = false)
+
+            // The super interface types must be non-null.
+            val superInterfaceType = fooClass.interfaceTypes().single()
+            assertNonNull(superInterfaceType, expectAnnotation = false)
+        }
+    }
 }
diff --git a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/typeitem/CommonTypeParameterItemTest.kt b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/typeitem/CommonTypeParameterItemTest.kt
index e3048e1..0059c62 100644
--- a/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/typeitem/CommonTypeParameterItemTest.kt
+++ b/metalava-model-testsuite/src/main/java/com/android/tools/metalava/model/testsuite/typeitem/CommonTypeParameterItemTest.kt
@@ -17,7 +17,6 @@
 package com.android.tools.metalava.model.testsuite.typeitem
 
 import com.android.tools.metalava.model.ClassTypeItem
-import com.android.tools.metalava.model.VariableTypeItem
 import com.android.tools.metalava.model.testsuite.BaseModelTest
 import com.android.tools.metalava.testing.java
 import com.android.tools.metalava.testing.kotlin
@@ -176,11 +175,9 @@
             assertThat(classTypeParamBound).isInstanceOf(ClassTypeItem::class.java)
             assertThat((classTypeParamBound as ClassTypeItem).qualifiedName)
                 .isEqualTo("test.pkg.Foo")
-            assertThat(classTypeParamBound.parameters).hasSize(1)
-            val classTypeParamBoundParam = classTypeParamBound.parameters.single()
-            assertThat(classTypeParamBoundParam).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((classTypeParamBoundParam as VariableTypeItem).asTypeParameter)
-                .isEqualTo(classTypeParam)
+            assertThat(classTypeParamBound.arguments).hasSize(1)
+            val classTypeParamBoundTypeArgument = classTypeParamBound.arguments.single()
+            classTypeParamBoundTypeArgument.assertReferencesTypeParameter(classTypeParam)
 
             val method = clazz.methods().single()
             val methodTypeParam = method.typeParameterList().typeParameters().single()
@@ -188,11 +185,9 @@
             assertThat(methodTypeParamBound).isInstanceOf(ClassTypeItem::class.java)
             assertThat((methodTypeParamBound as ClassTypeItem).qualifiedName)
                 .isEqualTo("test.pkg.Foo")
-            assertThat(methodTypeParamBound.parameters).hasSize(1)
-            val methodTypeParamBoundParam = methodTypeParamBound.parameters.single()
-            assertThat(methodTypeParamBoundParam).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((methodTypeParamBoundParam as VariableTypeItem).asTypeParameter)
-                .isEqualTo(methodTypeParam)
+            assertThat(methodTypeParamBound.arguments).hasSize(1)
+            val methodTypeParamBoundTypeArgument = methodTypeParamBound.arguments.single()
+            methodTypeParamBoundTypeArgument.assertReferencesTypeParameter(methodTypeParam)
         }
     }
 
@@ -231,13 +226,11 @@
 
             // A extends C
             val aBound = a.typeBounds().single()
-            assertThat(aBound).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((aBound as VariableTypeItem).asTypeParameter).isEqualTo(c)
+            aBound.assertReferencesTypeParameter(c)
 
             // B extends A
             val bBound = b.typeBounds().single()
-            assertThat(bBound).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((bBound as VariableTypeItem).asTypeParameter).isEqualTo(a)
+            bBound.assertReferencesTypeParameter(a)
 
             // C
             assertThat(c.typeBounds()).isEmpty()
@@ -283,9 +276,7 @@
             val methodTypeParam = method.typeParameterList().typeParameters().single()
             assertThat(methodTypeParam.toSource()).isEqualTo("E extends T")
             val methodTypeParamBound = methodTypeParam.typeBounds().single()
-            assertThat(methodTypeParamBound).isInstanceOf(VariableTypeItem::class.java)
-            assertThat((methodTypeParamBound as VariableTypeItem).asTypeParameter)
-                .isEqualTo(clazzTypeParam)
+            methodTypeParamBound.assertReferencesTypeParameter(clazzTypeParam)
         }
     }
 
@@ -425,10 +416,64 @@
             val typeParameter = method.typeParameterList().typeParameters().single()
             val typeVariable = method.returnType()
 
-            assertThat(typeVariable).isInstanceOf(VariableTypeItem::class.java)
-            val toType = typeParameter.toType()
-            assertThat(toType).isEqualTo(typeVariable)
-            assertThat(toType).isInstanceOf(VariableTypeItem::class.java)
+            typeVariable.assertReferencesTypeParameter(typeParameter)
+            assertThat(typeParameter.type()).isEqualTo(typeVariable)
+        }
+    }
+
+    @Test
+    fun `Test type parameter with annotations`() {
+        val typeParameterAnnotation =
+            java(
+                """
+                    package test.pkg;
+                    import java.lang.annotation.*;
+                    import static java.lang.annotation.ElementType.TYPE_PARAMETER;
+                    import static java.lang.annotation.RetentionPolicy.SOURCE;
+                    @Retention(SOURCE)
+                    @Target({TYPE_PARAMETER})
+                    public @interface TypeParameterAnnotation {
+                    }
+                """
+            )
+        runCodebaseTest(
+            inputSet(
+                typeParameterAnnotation,
+                java(
+                    """
+                        package test.pkg;
+                        public class Foo<@TypeParameterAnnotation T> {
+                            private Foo() {}
+                        }
+                    """
+                ),
+            ),
+            inputSet(
+                typeParameterAnnotation,
+                kotlin(
+                    """
+                        package test.pkg
+                        class Foo<@TypeParameterAnnotation T>
+                        private constructor()
+                    """
+                ),
+            ),
+            inputSet(
+                signature(
+                    """
+                        // Signature format: 2.0
+                        package test.pkg {
+                          public class Foo<@test.pkg.TypeParameterAnnotation T> {
+                          }
+                        }
+                    """
+                ),
+            ),
+        ) {
+            val fooClass = codebase.assertClass("test.pkg.Foo")
+            val typeParameter = fooClass.typeParameterList().typeParameters().single()
+            val annotation = typeParameter.modifiers.annotations().single()
+            assertThat(annotation.qualifiedName).isEqualTo("test.pkg.TypeParameterAnnotation")
         }
     }
 }
diff --git a/metalava-model-text/build.gradle.kts b/metalava-model-text/build.gradle.kts
index 04050b8..a933038 100644
--- a/metalava-model-text/build.gradle.kts
+++ b/metalava-model-text/build.gradle.kts
@@ -28,6 +28,7 @@
 dependencies {
     testFixturesImplementation(libs.junit4)
 
+    testImplementation(project(":metalava-testing"))
     testImplementation(testFixtures(project(":metalava-model")))
     testImplementation(libs.androidLintTests)
     testImplementation(libs.junit4)
diff --git a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/ApiFile.kt b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/ApiFile.kt
index 31902a8..345b092 100644
--- a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/ApiFile.kt
+++ b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/ApiFile.kt
@@ -20,10 +20,13 @@
 import com.android.tools.metalava.model.AnnotationItem.Companion.unshortenAnnotation
 import com.android.tools.metalava.model.AnnotationManager
 import com.android.tools.metalava.model.ArrayTypeItem
+import com.android.tools.metalava.model.BoundsTypeItem
 import com.android.tools.metalava.model.ClassItem
+import com.android.tools.metalava.model.ClassKind
 import com.android.tools.metalava.model.ClassResolver
+import com.android.tools.metalava.model.ClassTypeItem
+import com.android.tools.metalava.model.Codebase
 import com.android.tools.metalava.model.DefaultModifierList
-import com.android.tools.metalava.model.JAVA_LANG_ANNOTATION
 import com.android.tools.metalava.model.JAVA_LANG_DEPRECATED
 import com.android.tools.metalava.model.JAVA_LANG_ENUM
 import com.android.tools.metalava.model.JAVA_LANG_OBJECT
@@ -32,54 +35,76 @@
 import com.android.tools.metalava.model.MethodItem
 import com.android.tools.metalava.model.PrimitiveTypeItem
 import com.android.tools.metalava.model.PrimitiveTypeItem.Primitive
+import com.android.tools.metalava.model.ThrowableType
 import com.android.tools.metalava.model.TypeNullability
-import com.android.tools.metalava.model.TypeParameterItem
 import com.android.tools.metalava.model.TypeParameterList
-import com.android.tools.metalava.model.TypeParameterList.Companion.NONE
 import com.android.tools.metalava.model.VisibilityLevel
 import com.android.tools.metalava.model.isNullableAnnotation
 import com.android.tools.metalava.model.isNullnessAnnotation
 import com.android.tools.metalava.model.javaUnescapeString
 import com.android.tools.metalava.model.noOpAnnotationManager
-import com.android.tools.metalava.model.text.TextTypeParameterList.Companion.create
 import java.io.File
 import java.io.IOException
 import java.io.InputStream
 import java.io.StringReader
+import java.util.IdentityHashMap
 import kotlin.text.Charsets.UTF_8
 
 @MetalavaApi
 class ApiFile
 private constructor(
-    /** Implements [ResolverContext] interface */
-    override val classResolver: ClassResolver?,
-    private val formatForLegacyFiles: FileFormat?
+    private val codebase: TextCodebase,
+    private val formatForLegacyFiles: FileFormat?,
 ) : ResolverContext {
 
     /**
+     * Provides support for parsing and caching `TypeItem` instances.
+     *
+     * Defer creation until after the first file has been read and [kotlinStyleNulls] has been set
+     * to a non-null value to ensure that it picks up the correct setting of [kotlinStyleNulls].
+     */
+    private val typeParser by
+        lazy(LazyThreadSafetyMode.NONE) { TextTypeParser(codebase, kotlinStyleNulls!!) }
+
+    /**
      * Whether types should be interpreted to be in Kotlin format (e.g. ? suffix means nullable, !
      * suffix means unknown, and absence of a suffix means not nullable.
      *
      * Updated based on the header of the signature file being parsed.
      */
-    private var kotlinStyleNulls: Boolean = false
+    private var kotlinStyleNulls: Boolean? = null
 
     /** The file format of the file being parsed. */
     lateinit var format: FileFormat
 
+    /** Map from [ClassItem] to [TypeParameterScope]. */
+    private val classToTypeParameterScope = IdentityHashMap<ClassItem, TypeParameterScope>()
+
+    /**
+     * The set of interface types needed for later resolution.
+     *
+     * TODO(b/323516595): Find a better way.
+     */
+    private val interfaceTypesForResolution = mutableSetOf<ClassTypeItem>()
+
     private val mClassToSuper = HashMap<TextClassItem, String>(30000)
-    private val mClassToInterface = HashMap<TextClassItem, ArrayList<String>>(10000)
 
     companion object {
         /**
-         * Same as [.parseApi]}, but take a single file for convenience.
+         * Same as `parseApi(List<File>, ...)`, but takes a single file for convenience.
          *
          * @param file input signature file
          */
         fun parseApi(
             file: File,
             annotationManager: AnnotationManager,
-        ) = parseApi(listOf(file), annotationManager)
+            description: String? = null,
+        ) =
+            parseApi(
+                files = listOf(file),
+                annotationManager = annotationManager,
+                description = description,
+            )
 
         /**
          * Read API signature files into a [TextCodebase].
@@ -93,19 +118,28 @@
         fun parseApi(
             files: List<File>,
             annotationManager: AnnotationManager = noOpAnnotationManager,
+            description: String? = null,
             classResolver: ClassResolver? = null,
             formatForLegacyFiles: FileFormat? = null,
-        ): TextCodebase {
+            // Provides the called with access to the ApiFile.
+            apiStatsConsumer: (Stats) -> Unit = {},
+        ): Codebase {
             require(files.isNotEmpty()) { "files must not be empty" }
-            val api = TextCodebase(files[0], annotationManager)
-            val description = StringBuilder("Codebase loaded from ")
-            val parser = ApiFile(classResolver, formatForLegacyFiles)
+            val api =
+                TextCodebase(
+                    location = files[0],
+                    annotationManager = annotationManager,
+                    classResolver = classResolver,
+                )
+            val actualDescription =
+                description
+                    ?: buildString {
+                        append("Codebase loaded from ")
+                        files.joinTo(this)
+                    }
+            val parser = ApiFile(api, formatForLegacyFiles)
             var first = true
             for (file in files) {
-                if (!first) {
-                    description.append(", ")
-                }
-                description.append(file.path)
                 val apiText: String =
                     try {
                         file.readText(UTF_8)
@@ -116,11 +150,14 @@
                             cause = ex
                         )
                     }
-                parser.parseApiSingleFile(api, !first, file.path, apiText)
+                parser.parseApiSingleFile(!first, file.path, apiText)
                 first = false
             }
-            api.description = description.toString()
-            parser.postProcess(api)
+            api.description = actualDescription
+            parser.postProcess()
+
+            apiStatsConsumer(parser.stats)
+
             return api
         }
 
@@ -133,7 +170,7 @@
             filename: String,
             apiText: String,
             @Suppress("UNUSED_PARAMETER") kotlinStyleNulls: Boolean?,
-        ): TextCodebase {
+        ): Codebase {
             return parseApi(
                 filename,
                 apiText,
@@ -149,7 +186,7 @@
         @JvmStatic
         @MetalavaApi
         @Throws(ApiParseException::class)
-        fun parseApi(filename: String, inputStream: InputStream): TextCodebase {
+        fun parseApi(filename: String, inputStream: InputStream): Codebase {
             val apiText = inputStream.bufferedReader().readText()
             return parseApi(filename, apiText)
         }
@@ -160,26 +197,86 @@
             apiText: String,
             classResolver: ClassResolver? = null,
             formatForLegacyFiles: FileFormat? = null,
-        ): TextCodebase {
-            val api = TextCodebase(File(filename), noOpAnnotationManager)
+        ): Codebase {
+            val api =
+                TextCodebase(
+                    location = File(filename),
+                    annotationManager = noOpAnnotationManager,
+                    classResolver = classResolver,
+                )
             api.description = "Codebase loaded from $filename"
-            val parser = ApiFile(classResolver, formatForLegacyFiles)
-            parser.parseApiSingleFile(api, false, filename, apiText)
-            parser.postProcess(api)
+            val parser = ApiFile(api, formatForLegacyFiles)
+            parser.parseApiSingleFile(false, filename, apiText)
+            parser.postProcess()
             return api
         }
+
+        /**
+         * Extracts the bounds string list from the [typeParameterString].
+         *
+         * Given `T extends a.B & b.C<? super T>` this will return a list of `a.B` and `b.C<? super
+         * T>`.
+         */
+        fun extractTypeParameterBoundsStringList(typeParameterString: String?): List<String> {
+            val s = typeParameterString ?: return emptyList()
+            val index = s.indexOf("extends ")
+            if (index == -1) {
+                return emptyList()
+            }
+            val list = mutableListOf<String>()
+            var angleBracketBalance = 0
+            var start = index + "extends ".length
+            val length = s.length
+            for (i in start until length) {
+                val c = s[i]
+                if (c == '&' && angleBracketBalance == 0) {
+                    addNonBlankStringToList(list, typeParameterString, start, i)
+                    start = i + 1
+                } else if (c == '<') {
+                    angleBracketBalance++
+                } else if (c == '>') {
+                    angleBracketBalance--
+                    if (angleBracketBalance == 0) {
+                        addNonBlankStringToList(list, typeParameterString, start, i + 1)
+                        start = i + 1
+                    }
+                }
+            }
+            if (start < length) {
+                addNonBlankStringToList(list, typeParameterString, start, length)
+            }
+            return list
+        }
+
+        private fun addNonBlankStringToList(
+            list: MutableList<String>,
+            s: String,
+            from: Int,
+            to: Int
+        ) {
+            val element = s.substring(from, to).trim()
+            if (element.isNotEmpty()) list.add(element)
+        }
     }
 
     /**
      * Perform any final steps to initialize the [TextCodebase] after parsing the signature files.
      */
-    private fun postProcess(api: TextCodebase) {
+    private fun postProcess() {
         // Use this as the context for resolving references.
-        ReferenceResolver.resolveReferences(this, api)
+        ReferenceResolver.resolveReferences(this, codebase, typeParser) {
+            typeParameterScopeForClass(it)
+        }
+
+        // Resolve all interface types that were found in the signature file.
+        // TODO(b/323516595): Find a better way.
+        for (interfaceType in interfaceTypesForResolution) {
+            // Resolve the interface type to a class.
+            interfaceType.asClass()
+        }
     }
 
     private fun parseApiSingleFile(
-        api: TextCodebase,
         appending: Boolean,
         filename: String,
         apiText: String,
@@ -189,8 +286,15 @@
         format =
             FileFormat.parseHeader(filename, StringReader(apiText), formatForLegacyFiles)
                 ?: FileFormat.V2
-        kotlinStyleNulls = format.kotlinStyleNulls
-        api.typeResolver.kotlinStyleNulls = kotlinStyleNulls
+
+        // Disallow a mixture of kotlinStyleNulls settings.
+        if (kotlinStyleNulls == null) {
+            kotlinStyleNulls = format.kotlinStyleNulls
+        } else if (kotlinStyleNulls != format.kotlinStyleNulls) {
+            throw ApiParseException(
+                "Cannot mix signature files with different settings of kotlinStyleNulls"
+            )
+        }
 
         if (appending) {
             // When we're appending, and the content is empty, nothing to do.
@@ -204,56 +308,48 @@
             val token = tokenizer.getToken() ?: break
             // TODO: Accept annotations on packages.
             if ("package" == token) {
-                parsePackage(api, tokenizer)
+                parsePackage(tokenizer)
             } else {
                 throw ApiParseException("expected package got $token", tokenizer)
             }
         }
     }
 
-    private fun parsePackage(api: TextCodebase, tokenizer: Tokenizer) {
-        var pkg: TextPackageItem
+    private fun parsePackage(tokenizer: Tokenizer) {
         var token: String = tokenizer.requireToken()
 
         // Metalava: including annotations in file now
         val annotations: List<String> = getAnnotations(tokenizer, token)
-        val modifiers = DefaultModifierList(api, DefaultModifierList.PUBLIC, null)
+        val modifiers = DefaultModifierList(codebase, DefaultModifierList.PUBLIC, null)
         modifiers.addAnnotations(annotations)
         token = tokenizer.current
         tokenizer.assertIdent(token)
         val name: String = token
 
         // If the same package showed up multiple times, make sure they have the same modifiers.
-        // (Packages can't have public/private/etc, but they can have annotations, which are part of
-        // ModifierList.)
-        // ModifierList doesn't provide equals(), neither does AnnotationItem which ModifierList
-        // contains,
-        // so we just use toString() here for equality comparison.
-        // However, ModifierList.toString() throws if the owner is not yet set, so we have to
-        // instantiate an
-        // (owner) TextPackageItem here.
-        // If it's a duplicate package, then we'll replace pkg with the existing one in the
-        // following if block.
-
-        // TODO: However, currently this parser can't handle annotations on packages, so we will
-        // never hit this case.
-        // Once the parser supports that, we should add a test case for this too.
-        pkg = TextPackageItem(api, name, modifiers, tokenizer.pos())
-        val existing = api.findPackage(name)
-        if (existing != null) {
-            if (pkg.modifiers != existing.modifiers) {
-                throw ApiParseException(
-                    String.format(
-                        "Contradicting declaration of package %s. Previously seen with modifiers \"%s\", but now with \"%s\"",
-                        name,
-                        pkg.modifiers,
-                        modifiers
-                    ),
-                    tokenizer
-                )
+        // (Packages can't have public/private/etc., but they can have annotations, which are part
+        // of ModifierList.)
+        val existing = codebase.findPackage(name)
+        val pkg =
+            if (existing != null) {
+                if (modifiers != existing.modifiers) {
+                    throw ApiParseException(
+                        String.format(
+                            "Contradicting declaration of package %s. Previously seen with modifiers \"%s\", but now with \"%s\"",
+                            name,
+                            existing.modifiers,
+                            modifiers
+                        ),
+                        tokenizer
+                    )
+                }
+                existing
+            } else {
+                val newPackageItem = TextPackageItem(codebase, name, modifiers, tokenizer.pos())
+                codebase.addPackage(newPackageItem)
+                newPackageItem
             }
-            pkg = existing
-        }
+
         token = tokenizer.requireToken()
         if ("{" != token) {
             throw ApiParseException("expected '{' got $token", tokenizer)
@@ -263,70 +359,52 @@
             if ("}" == token) {
                 break
             } else {
-                parseClass(api, pkg, tokenizer, token)
+                parseClass(pkg, tokenizer, token)
             }
         }
-        api.addPackage(pkg)
     }
 
     private fun mapClassToSuper(classInfo: TextClassItem, superclass: String?) {
         superclass?.let { mClassToSuper.put(classInfo, superclass) }
     }
 
-    private fun mapClassToInterface(classInfo: TextClassItem, iface: String) {
-        if (!mClassToInterface.containsKey(classInfo)) {
-            mClassToInterface[classInfo] = ArrayList()
-        }
-        mClassToInterface[classInfo]?.let { if (!it.contains(iface)) it.add(iface) }
-    }
-
-    private fun implementsInterface(classInfo: TextClassItem, iface: String): Boolean {
-        return mClassToInterface[classInfo]?.contains(iface) ?: false
-    }
-
     /** Implements [ResolverContext] interface */
-    override fun namesOfInterfaces(cl: TextClassItem): List<String>? = mClassToInterface[cl]
+    override fun superClassTypeString(cl: ClassItem): String? = mClassToSuper[cl]
 
-    /** Implements [ResolverContext] interface */
-    override fun nameOfSuperClass(cl: TextClassItem): String? = mClassToSuper[cl]
-
-    private fun parseClass(
-        api: TextCodebase,
-        pkg: TextPackageItem,
-        tokenizer: Tokenizer,
-        startingToken: String
-    ) {
+    private fun parseClass(pkg: TextPackageItem, tokenizer: Tokenizer, startingToken: String) {
         var token = startingToken
-        var isInterface = false
-        var isAnnotation = false
-        var isEnum = false
-        var ext: String? = null
+        var classKind = ClassKind.CLASS
+        var superClassTypeString: String? = null
 
         // Metalava: including annotations in file now
         val annotations: List<String> = getAnnotations(tokenizer, token)
         token = tokenizer.current
-        val modifiers = parseModifiers(api, tokenizer, token, annotations)
+        val modifiers = parseModifiers(tokenizer, token, annotations)
+
+        // Remember this position as this seems like a good place to use to report issues with the
+        // class item.
+        val classPosition = tokenizer.pos()
+
         token = tokenizer.current
         when (token) {
             "class" -> {
                 token = tokenizer.requireToken()
             }
             "interface" -> {
-                isInterface = true
+                classKind = ClassKind.INTERFACE
                 modifiers.setAbstract(true)
                 token = tokenizer.requireToken()
             }
             "@interface" -> {
-                // Annotation
+                classKind = ClassKind.ANNOTATION_TYPE
                 modifiers.setAbstract(true)
-                isAnnotation = true
                 token = tokenizer.requireToken()
             }
             "enum" -> {
-                isEnum = true
+                classKind = ClassKind.ENUM
                 modifiers.setFinal(true)
                 modifiers.setStatic(true)
-                ext = JAVA_LANG_ENUM
+                superClassTypeString = JAVA_LANG_ENUM
                 token = tokenizer.requireToken()
             }
             else -> {
@@ -334,148 +412,351 @@
             }
         }
         tokenizer.assertIdent(token)
-        // The classType and qualifiedClassType include the type parameter string, the className and
-        // qualifiedClassName are just the name without type parameters.
-        val classType: String = token
-        val (className, typeParameters) = parseClassName(api, classType)
-        val qualifiedClassType = qualifiedName(pkg.name(), classType)
-        val qualifiedClassName = qualifiedName(pkg.name(), className)
-        token = tokenizer.requireToken()
-        var cl =
-            TextClassItem(
-                api,
-                tokenizer.pos(),
-                modifiers,
-                isInterface,
-                isEnum,
-                isAnnotation,
-                qualifiedClassName,
-                qualifiedClassType,
-                className,
-                annotations,
-                typeParameters
-            )
 
-        cl.setContainingPackage(pkg)
-        if ("extends" == token && !isInterface) {
-            token = getAnnotationCompleteToken(tokenizer, tokenizer.requireToken())
-            var superClassName = token
-            // Make sure full super class name is found if there are type use annotations.
-            // This can't use [parseType] because the next token might be a separate type (classes
-            // only have a single `extends` type, but all interface supertypes are listed as
-            // `extends` instead of `implements`).
-            // However, this type cannot be an array, so unlike [parseType] this does not need to
-            // check if the next token has annotations.
-            while (isIncompleteTypeToken(token)) {
-                token = getAnnotationCompleteToken(tokenizer, tokenizer.current)
-                superClassName += " $token"
-            }
-            ext = superClassName
+        // The declaredClassType consists of the full name (i.e. preceded by the containing class's
+        // full name followed by a '.' if there is one) plus the type parameter string.
+        val declaredClassType: String = token
+
+        // Extract lots of information from the declared class type.
+        val (
+            className,
+            fullName,
+            qualifiedClassName,
+            outerClass,
+            typeParameterList,
+            typeParameterScope,
+        ) = parseDeclaredClassType(pkg, declaredClassType, classPosition)
+
+        token = tokenizer.requireToken()
+
+        if ("extends" == token && classKind != ClassKind.INTERFACE) {
+            superClassTypeString = parseSuperTypeString(tokenizer, tokenizer.requireToken())
             token = tokenizer.current
         }
+
+        val interfaceTypes = mutableSetOf<ClassTypeItem>()
         if ("implements" == token || "extends" == token) {
             token = tokenizer.requireToken()
             while (true) {
                 if ("{" == token) {
                     break
                 } else if ("," != token) {
-                    var interfaceName = getAnnotationCompleteToken(tokenizer, token)
-                    // Make sure full interface name is found if there are type use annotations.
-                    // This can't use [parseType] because the next token might be a separate type.
-                    // However, this type cannot be an array, so unlike [parseType] this does not
-                    // need to check if the next token has annotations.
-                    while (isIncompleteTypeToken(token)) {
-                        token = getAnnotationCompleteToken(tokenizer, tokenizer.current)
-                        interfaceName += " $token"
-                    }
-                    mapClassToInterface(cl, interfaceName)
+                    val interfaceTypeString = parseSuperTypeString(tokenizer, token)
+                    val interfaceType =
+                        typeParser.getSuperType(interfaceTypeString, typeParameterScope)
+                    interfaceTypes.add(interfaceType)
                     token = tokenizer.current
                 } else {
                     token = tokenizer.requireToken()
                 }
             }
         }
-        if (JAVA_LANG_ENUM == ext) {
-            cl.setIsEnum(true)
-            // Above we marked all enums as static but for a top level class it's implicit
-            if (!cl.fullName().contains(".")) {
-                cl.modifiers.setStatic(false)
-            }
-        } else if (isAnnotation) {
-            mapClassToInterface(cl, JAVA_LANG_ANNOTATION)
-        } else if (implementsInterface(cl, JAVA_LANG_ANNOTATION)) {
-            cl.setIsAnnotationType(true)
+        if (JAVA_LANG_ENUM == superClassTypeString) {
+            // This can be taken either for an enum class, or a normal class that extends
+            // java.lang.Enum (which was the old way of representing an enum in the API signature
+            // files.
+            classKind = ClassKind.ENUM
+        } else if (classKind == ClassKind.ANNOTATION_TYPE) {
+            // If the annotation was defined using @interface then add the implicit
+            // "implements java.lang.annotation.Annotation".
+            interfaceTypes.add(typeParser.superAnnotationType)
+        } else if (typeParser.superAnnotationType in interfaceTypes) {
+            // A normal class that implements java.lang.annotation.Annotation which was the old way
+            // of representing an annotation in the API signature files. So, update the class kind
+            // to match.
+            classKind = ClassKind.ANNOTATION_TYPE
         }
+
         if ("{" != token) {
             throw ApiParseException("expected {, was $token", tokenizer)
         }
-        token = tokenizer.requireToken()
-        cl =
-            when (val foundClass = api.findClass(cl.qualifiedName())) {
-                null -> {
-                    // Duplicate class is not found, thus update super class string
-                    // and keep cl
-                    mapClassToSuper(cl, ext)
-                    cl
-                }
-                else -> {
-                    if (!foundClass.isCompatible(cl)) {
-                        throw ApiParseException("Incompatible $foundClass definitions", cl.position)
-                    } else if (mClassToSuper[foundClass] != ext) {
-                        // Duplicate class with conflicting superclass names are found.
-                        // Since the clas definition found later should be prioritized,
-                        // overwrite the superclass name as ext but set cl as
-                        // foundClass, where the class attributes are stored
-                        // and continue to add methods/fields in foundClass
-                        mapClassToSuper(cl, ext)
-                        foundClass
-                    } else {
-                        foundClass
-                    }
-                }
-            }
+
+        // Above we marked all enums as static but for a top level class it's implicit
+        if (classKind == ClassKind.ENUM && !fullName.contains(".")) {
+            modifiers.setStatic(false)
+        }
+
+        // Get the characteristics of the class being added as they may be needed to compare against
+        // the characteristics of the same class from a previously processed signature file.
+        val newClassCharacteristics =
+            ClassCharacteristics(
+                position = classPosition,
+                qualifiedName = qualifiedClassName,
+                fullName = fullName,
+                classKind = classKind,
+                modifiers = modifiers,
+                superClassTypeString = superClassTypeString,
+            )
+
+        // Check to see if there is an existing class, if so merge this class definition into that
+        // one and return. Otherwise, drop through and create a whole new class.
+        if (tryMergingIntoExistingClass(tokenizer, newClassCharacteristics)) {
+            return
+        }
+
+        // Create the TextClassItem and set its package but do not add it to the package or
+        // register it.
+        val cl =
+            TextClassItem(
+                codebase = codebase,
+                position = classPosition,
+                modifiers = modifiers,
+                classKind = classKind,
+                qualifiedName = qualifiedClassName,
+                simpleName = className,
+                fullName = fullName,
+                annotations = annotations,
+                typeParameterList = typeParameterList,
+            )
+
+        cl.setInterfaceTypes(interfaceTypes.toList())
+
+        // Save the interface types to later when they will be resolved. That is needed to avoid
+        // later changes to the model which would/could cause concurrent modification issues.
+        // TODO(b/323516595): Find a better way.
+        interfaceTypesForResolution.addAll(interfaceTypes)
+
+        // Store the [TypeParameterScope] for this [ClassItem] so it can be retrieved later in
+        // [typeParameterScopeFromClass].
+        if (!typeParameterScope.isEmpty()) {
+            classToTypeParameterScope[cl] = typeParameterScope
+        }
+
+        cl.setContainingPackage(pkg)
+        cl.containingClass = outerClass
+        if (outerClass == null) {
+            // Add the class to the package, it will only be added to the TextCodebase once the
+            // package
+            // body has been parsed.
+            pkg.addClass(cl)
+        } else {
+            outerClass.addInnerClass(cl)
+        }
+        codebase.registerClass(cl)
+
+        // Record the super class type string as needing to be resolved for this class.
+        mapClassToSuper(cl, superClassTypeString)
+
+        // Parse the class body adding each member created to the class item being populated.
+        parseClassBody(tokenizer, cl, typeParameterScope)
+    }
+
+    /**
+     * Try merging the new class into an existing class that was previously loaded from a separate
+     * signature file.
+     *
+     * Will throw an exception if there is an existing class but it is not compatible with the new
+     * class.
+     *
+     * @return `false` if there is no existing class, `true` if there is and the merge succeeded.
+     */
+    private fun tryMergingIntoExistingClass(
+        tokenizer: Tokenizer,
+        newClassCharacteristics: ClassCharacteristics,
+    ): Boolean {
+        // Check for the existing class from a previously parsed file. If it could not be found
+        // then return.
+        val existingClass =
+            codebase.findClassInCodebase(newClassCharacteristics.qualifiedName) ?: return false
+
+        // Make sure the new class characteristics are compatible with the old class
+        // characteristic.
+        val existingCharacteristics = ClassCharacteristics.of(existingClass)
+        if (!existingCharacteristics.isCompatible(newClassCharacteristics)) {
+            throw ApiParseException(
+                "Incompatible $existingClass definitions",
+                newClassCharacteristics.position
+            )
+        }
+
+        // Use the latest super class.
+        val newSuperClassTypeString = newClassCharacteristics.superClassTypeString
+        if (mClassToSuper[existingClass] != newSuperClassTypeString) {
+            // Duplicate class with conflicting superclass names are found. Since the class
+            // definition found later should be prioritized, overwrite the superclass name.
+            mapClassToSuper(existingClass, newSuperClassTypeString)
+        }
+
+        // Parse the class body adding each member created to the existing class.
+        parseClassBody(tokenizer, existingClass, typeParameterScopeForClass(existingClass))
+
+        return true
+    }
+
+    /** Get the [TypeParameterScope] for a previously created [ClassItem]. */
+    private fun typeParameterScopeForClass(classItem: ClassItem?): TypeParameterScope =
+        classItem?.let { classToTypeParameterScope[classItem] } ?: TypeParameterScope.empty
+
+    /** Parse the class body, adding members to [cl]. */
+    private fun parseClassBody(
+        tokenizer: Tokenizer,
+        cl: TextClassItem,
+        classTypeParameterScope: TypeParameterScope,
+    ) {
+        var token = tokenizer.requireToken()
         while (true) {
             if ("}" == token) {
                 break
             } else if ("ctor" == token) {
                 token = tokenizer.requireToken()
-                parseConstructor(api, tokenizer, cl, token)
+                parseConstructor(tokenizer, cl, classTypeParameterScope, token)
             } else if ("method" == token) {
                 token = tokenizer.requireToken()
-                parseMethod(api, tokenizer, cl, token)
+                parseMethod(tokenizer, cl, classTypeParameterScope, token)
             } else if ("field" == token) {
                 token = tokenizer.requireToken()
-                parseField(api, tokenizer, cl, token, false)
+                parseField(tokenizer, cl, classTypeParameterScope, token, false)
             } else if ("enum_constant" == token) {
                 token = tokenizer.requireToken()
-                parseField(api, tokenizer, cl, token, true)
+                parseField(tokenizer, cl, classTypeParameterScope, token, true)
             } else if ("property" == token) {
                 token = tokenizer.requireToken()
-                parseProperty(api, tokenizer, cl, token)
+                parseProperty(tokenizer, cl, classTypeParameterScope, token)
             } else {
                 throw ApiParseException("expected ctor, enum_constant, field or method", tokenizer)
             }
             token = tokenizer.requireToken()
         }
-        pkg.addClass(cl)
     }
 
     /**
-     * Splits the class type into its name and type parameter list.
-     *
-     * For example "Foo" would split into name "Foo" and an empty type parameter list, while "Foo<A,
-     * B extends java.lang.String, C>" would split into name "Foo" and type parameter list with "A",
-     * "B extends java.lang.String", and "C" as type parameters.
+     * Parse a super type string, i.e. a string representing a super class type or a super interface
+     * type.
      */
-    private fun parseClassName(api: TextCodebase, type: String): Pair<String, TypeParameterList> {
-        val paramIndex = type.indexOf('<')
-        return if (paramIndex == -1) {
-            Pair(type, NONE)
+    private fun parseSuperTypeString(tokenizer: Tokenizer, initialToken: String): String {
+        var token = getAnnotationCompleteToken(tokenizer, initialToken)
+
+        // Use the token directly if it is complete, otherwise construct the super class type
+        // string from as many tokens as necessary.
+        return if (!isIncompleteTypeToken(token)) {
+            token
         } else {
-            Pair(type.substring(0, paramIndex), create(api, type.substring(paramIndex)))
+            buildString {
+                append(token)
+
+                // Make sure full super class name is found if there are type use
+                // annotations. This can't use [parseType] because the next token might be a
+                // separate type (classes only have a single `extends` type, but all
+                // interface supertypes are listed as `extends` instead of `implements`).
+                // However, this type cannot be an array, so unlike [parseType] this does
+                // not need to check if the next token has annotations.
+                do {
+                    token = getAnnotationCompleteToken(tokenizer, tokenizer.current)
+                    append(" ")
+                    append(token)
+                } while (isIncompleteTypeToken(token))
+            }
         }
     }
 
+    /** Encapsulates multiple return values from [parseDeclaredClassType]. */
+    private data class DeclaredClassTypeComponents(
+        /** The simple name of the class, i.e. not including any outer class prefix. */
+        val simpleName: String,
+        /** The full name of the class, including outer class prefix. */
+        val fullName: String,
+        /** The fully qualified name, including package and full name. */
+        val qualifiedName: String,
+        /** The optional, resolved outer [ClassItem]. */
+        val outerClass: ClassItem?,
+        /** The set of type parameters. */
+        val typeParameterList: TypeParameterList,
+        /** The [TypeParameterScope] including [typeParameterList]. */
+        val typeParameterScope: TypeParameterScope,
+    )
+
+    /**
+     * Splits the declared class type into [DeclaredClassTypeComponents].
+     *
+     * For example "Foo" would split into full name "Foo" and an empty type parameter list, while
+     * `"Foo.Bar<A, B extends java.lang.String, C>"` would split into full name `"Foo.Bar"` and type
+     * parameter list with `"A"`,`"B extends java.lang.String"`, and `"C"` as type parameters.
+     *
+     * If the qualified name matches an existing class then return its information.
+     */
+    private fun parseDeclaredClassType(
+        pkg: TextPackageItem,
+        declaredClassType: String,
+        classPosition: SourcePositionInfo,
+    ): DeclaredClassTypeComponents {
+        // Split the declared class type into full name and type parameters.
+        val paramIndex = declaredClassType.indexOf('<')
+        val (fullName, typeParameterListString) =
+            if (paramIndex == -1) {
+                Pair(declaredClassType, "")
+            } else {
+                Pair(
+                    declaredClassType.substring(0, paramIndex),
+                    declaredClassType.substring(paramIndex)
+                )
+            }
+        val pkgName = pkg.name()
+        val qualifiedName = qualifiedName(pkgName, fullName)
+
+        // Split the full name into an optional outer class and a simple name.
+        val nestedClassIndex = fullName.lastIndexOf('.')
+        val (outerClass, simpleName) =
+            if (nestedClassIndex == -1) {
+                Pair(null, fullName)
+            } else {
+                val outerClassFullName = fullName.substring(0, nestedClassIndex)
+                val qualifiedOuterClassName = qualifiedName(pkgName, outerClassFullName)
+
+                // Search for the outer class in the codebase. This is safe as the outer class
+                // always precedes its nested classes.
+                val outerClass =
+                    codebase.getOrCreateClass(qualifiedOuterClassName, isOuterClass = true)
+
+                val innerClassName = fullName.substring(nestedClassIndex + 1)
+                Pair(outerClass, innerClassName)
+            }
+
+        // Get the [TypeParameterScope] for the outer class, if any, from a previously stored one,
+        // otherwise use the empty scope as the [ClassItem] is a stub and so has no type parameters.
+        val outerClassTypeParameterScope = typeParameterScopeForClass(outerClass)
+
+        // Create type parameter list and scope from the string and optional outer class scope.
+        val (typeParameterList, typeParameterScope) =
+            if (typeParameterListString == "")
+                Pair(TypeParameterList.NONE, outerClassTypeParameterScope)
+            else createTypeParameterList(outerClassTypeParameterScope, typeParameterListString)
+
+        // Decide which type parameter list and scope to actually use.
+        //
+        // If the class already exists then reuse its type parameter list and scope, otherwise use
+        // the newly created one.
+        //
+        // The reason for this is that otherwise any types parsed with the newly created scope would
+        // reference type parameters in the newly created list which are different to the ones
+        // belonging to the existing class.
+        val (actualTypeParameterList, actualTypeParameterScope) =
+            codebase.findClassInCodebase(qualifiedName)?.let { existingClass ->
+                // Check to make sure that the type parameter lists are the same.
+                val existingTypeParameterList = existingClass.typeParameterList
+                val existingTypeParameterListString = existingTypeParameterList.toString()
+                val normalizedTypeParameterListString = typeParameterList.toString()
+                if (!normalizedTypeParameterListString.equals(existingTypeParameterListString)) {
+                    val location = existingClass.location()
+                    throw ApiParseException(
+                        "Inconsistent type parameter list for $qualifiedName, this has $normalizedTypeParameterListString but it was previously defined as $existingTypeParameterListString at ${location.path}:${location.line}",
+                        classPosition
+                    )
+                }
+
+                Pair(existingTypeParameterList, typeParameterScopeForClass(existingClass))
+            }
+                ?: Pair(typeParameterList, typeParameterScope)
+
+        return DeclaredClassTypeComponents(
+            simpleName = simpleName,
+            fullName = fullName,
+            qualifiedName = qualifiedName,
+            outerClass = outerClass,
+            typeParameterList = actualTypeParameterList,
+            typeParameterScope = actualTypeParameterScope,
+        )
+    }
+
     /**
      * If the [startingToken] contains the beginning of an annotation, pulls additional tokens from
      * [tokenizer] to complete the annotation, returning the full token. If there isn't an
@@ -555,40 +836,49 @@
     }
 
     private fun parseConstructor(
-        api: TextCodebase,
         tokenizer: Tokenizer,
         cl: TextClassItem,
+        classTypeParameterScope: TypeParameterScope,
         startingToken: String
     ) {
         var token = startingToken
         val method: TextConstructorItem
-        var typeParameterList = NONE
 
         // Metalava: including annotations in file now
         val annotations: List<String> = getAnnotations(tokenizer, token)
         token = tokenizer.current
-        val modifiers = parseModifiers(api, tokenizer, token, annotations)
+        val modifiers = parseModifiers(tokenizer, token, annotations)
         token = tokenizer.current
-        if ("<" == token) {
-            typeParameterList = parseTypeParameterList(api, tokenizer)
-            token = tokenizer.requireToken()
-        }
+
+        // Get a TypeParameterList and accompanying TypeParameterScope
+        val (typeParameterList, typeParameterScope) =
+            if ("<" == token) {
+                parseTypeParameterList(tokenizer, classTypeParameterScope).also {
+                    token = tokenizer.requireToken()
+                }
+            } else {
+                Pair(TypeParameterList.NONE, classTypeParameterScope)
+            }
+
         tokenizer.assertIdent(token)
         val name: String =
             token.substring(
                 token.lastIndexOf('.') + 1
             ) // For inner classes, strip outer classes from name
-        // Collect all type parameters in scope into one list
-        val typeParams = typeParameterList.typeParameters() + cl.typeParameterList.typeParameters()
-        val parameters = parseParameterList(api, tokenizer, typeParams)
+        val parameters = parseParameterList(tokenizer, typeParameterScope)
         // Constructors cannot return null.
-        val ctorReturn = cl.toType().duplicate(TypeNullability.NONNULL)
+        val ctorReturn = cl.type().duplicate(TypeNullability.NONNULL)
         method =
-            TextConstructorItem(api, name, cl, modifiers, ctorReturn, parameters, tokenizer.pos())
+            TextConstructorItem(
+                codebase,
+                name,
+                cl,
+                modifiers,
+                ctorReturn,
+                parameters,
+                tokenizer.pos()
+            )
         method.setTypeParameterList(typeParameterList)
-        if (typeParameterList is TextTypeParameterList) {
-            typeParameterList.setOwner(method)
-        }
         token = tokenizer.requireToken()
         if ("throws" == token) {
             token = parseThrows(tokenizer, method)
@@ -602,27 +892,31 @@
     }
 
     private fun parseMethod(
-        api: TextCodebase,
         tokenizer: Tokenizer,
         cl: TextClassItem,
+        classTypeParameterScope: TypeParameterScope,
         startingToken: String
     ) {
         var token = startingToken
         val method: TextMethodItem
-        var typeParameterList = NONE
 
         // Metalava: including annotations in file now
         val annotations = getAnnotations(tokenizer, token)
         token = tokenizer.current
-        val modifiers = parseModifiers(api, tokenizer, token, null)
+        val modifiers = parseModifiers(tokenizer, token, null)
         token = tokenizer.current
-        if ("<" == token) {
-            typeParameterList = parseTypeParameterList(api, tokenizer)
-            token = tokenizer.requireToken()
-        }
+
+        // Get a TypeParameterList and accompanying TypeParameterScope
+        val (typeParameterList, typeParameterScope) =
+            if ("<" == token) {
+                parseTypeParameterList(tokenizer, classTypeParameterScope).also {
+                    token = tokenizer.requireToken()
+                }
+            } else {
+                Pair(TypeParameterList.NONE, classTypeParameterScope)
+            }
+
         tokenizer.assertIdent(token)
-        // Collect all type parameters in scope into one list
-        val typeParams = typeParameterList.typeParameters() + cl.typeParameterList.typeParameters()
 
         val returnType: TextTypeItem
         val parameters: List<TextParameterItem>
@@ -630,7 +924,7 @@
         if (format.kotlinNameTypeOrder) {
             // Kotlin style: parse the name, the parameter list, then the return type.
             name = token
-            parameters = parseParameterList(api, tokenizer, typeParams)
+            parameters = parseParameterList(tokenizer, typeParameterScope)
             token = tokenizer.requireToken()
             if (token != ":") {
                 throw ApiParseException(
@@ -640,29 +934,27 @@
             }
             token = tokenizer.requireToken()
             tokenizer.assertIdent(token)
-            returnType = parseType(api, tokenizer, token, typeParams, annotations)
+            returnType = parseType(tokenizer, token, typeParameterScope, annotations)
             // TODO(b/300081840): update nullability handling
             modifiers.addAnnotations(annotations)
             token = tokenizer.current
         } else {
             // Java style: parse the return type, the name, and then the parameter list.
-            returnType = parseType(api, tokenizer, token, typeParams, annotations)
+            returnType = parseType(tokenizer, token, typeParameterScope, annotations)
             modifiers.addAnnotations(annotations)
             token = tokenizer.current
             tokenizer.assertIdent(token)
             name = token
-            parameters = parseParameterList(api, tokenizer, typeParams)
+            parameters = parseParameterList(tokenizer, typeParameterScope)
             token = tokenizer.requireToken()
         }
 
         if (cl.isInterface() && !modifiers.isDefault() && !modifiers.isStatic()) {
             modifiers.setAbstract(true)
         }
-        method = TextMethodItem(api, name, cl, modifiers, returnType, parameters, tokenizer.pos())
+        method =
+            TextMethodItem(codebase, name, cl, modifiers, returnType, parameters, tokenizer.pos())
         method.setTypeParameterList(typeParameterList)
-        if (typeParameterList is TextTypeParameterList) {
-            typeParameterList.setOwner(method)
-        }
         if ("throws" == token) {
             token = parseThrows(tokenizer, method)
         }
@@ -689,16 +981,16 @@
     }
 
     private fun parseField(
-        api: TextCodebase,
         tokenizer: Tokenizer,
         cl: TextClassItem,
+        classTypeParameterScope: TypeParameterScope,
         startingToken: String,
         isEnum: Boolean
     ) {
         var token = startingToken
         val annotations = getAnnotations(tokenizer, token)
         token = tokenizer.current
-        val modifiers = parseModifiers(api, tokenizer, token, null)
+        val modifiers = parseModifiers(tokenizer, token, null)
         token = tokenizer.current
         tokenizer.assertIdent(token)
 
@@ -709,15 +1001,13 @@
             name = parseNameWithColon(token, tokenizer)
             token = tokenizer.requireToken()
             tokenizer.assertIdent(token)
-            type =
-                parseType(api, tokenizer, token, cl.typeParameterList.typeParameters(), annotations)
+            type = parseType(tokenizer, token, classTypeParameterScope, annotations)
             // TODO(b/300081840): update nullability handling
             modifiers.addAnnotations(annotations)
             token = tokenizer.current
         } else {
             // Java style: parse the name, then the type.
-            type =
-                parseType(api, tokenizer, token, cl.typeParameterList.typeParameters(), annotations)
+            type = parseType(tokenizer, token, classTypeParameterScope, annotations)
             modifiers.addAnnotations(annotations)
             token = tokenizer.current
             tokenizer.assertIdent(token)
@@ -732,7 +1022,7 @@
             token = tokenizer.requireToken()
             // If this is an implicitly null constant, add the nullability.
             if (
-                !kotlinStyleNulls &&
+                !typeParser.kotlinStyleNulls &&
                     modifiers.isFinal() &&
                     value != null &&
                     type.modifiers.nullability() != TypeNullability.NONNULL
@@ -743,7 +1033,7 @@
         if (";" != token) {
             throw ApiParseException("expected ; found $token", tokenizer)
         }
-        val field = TextFieldItem(api, name, cl, modifiers, type, value, tokenizer.pos())
+        val field = TextFieldItem(codebase, name, cl, modifiers, type, value, tokenizer.pos())
         if (isEnum) {
             cl.addEnumConstant(field)
         } else {
@@ -752,13 +1042,12 @@
     }
 
     private fun parseModifiers(
-        api: TextCodebase,
         tokenizer: Tokenizer,
         startingToken: String?,
         annotations: List<String>?
     ): DefaultModifierList {
         var token = startingToken
-        val modifiers = DefaultModifierList(api, DefaultModifierList.PACKAGE_PRIVATE, null)
+        val modifiers = DefaultModifierList(codebase, DefaultModifierList.PACKAGE_PRIVATE, null)
         processModifiers@ while (true) {
             token =
                 when (token) {
@@ -918,9 +1207,9 @@
     }
 
     private fun parseProperty(
-        api: TextCodebase,
         tokenizer: Tokenizer,
         cl: TextClassItem,
+        classTypeParameterScope: TypeParameterScope,
         startingToken: String
     ) {
         var token = startingToken
@@ -928,7 +1217,7 @@
         // Metalava: including annotations in file now
         val annotations = getAnnotations(tokenizer, token)
         token = tokenizer.current
-        val modifiers = parseModifiers(api, tokenizer, token, null)
+        val modifiers = parseModifiers(tokenizer, token, null)
         token = tokenizer.current
         tokenizer.assertIdent(token)
 
@@ -939,15 +1228,13 @@
             name = parseNameWithColon(token, tokenizer)
             token = tokenizer.requireToken()
             tokenizer.assertIdent(token)
-            type =
-                parseType(api, tokenizer, token, cl.typeParameterList.typeParameters(), annotations)
+            type = parseType(tokenizer, token, classTypeParameterScope, annotations)
             // TODO(b/300081840): update nullability handling
             modifiers.addAnnotations(annotations)
             token = tokenizer.current
         } else {
             // Java style: parse the type, then the name.
-            type =
-                parseType(api, tokenizer, token, cl.typeParameterList.typeParameters(), annotations)
+            type = parseType(tokenizer, token, classTypeParameterScope, annotations)
             modifiers.addAnnotations(annotations)
             token = tokenizer.current
             tokenizer.assertIdent(token)
@@ -958,14 +1245,14 @@
         if (";" != token) {
             throw ApiParseException("expected ; found $token", tokenizer)
         }
-        val property = TextPropertyItem(api, name, cl, modifiers, type, tokenizer.pos())
+        val property = TextPropertyItem(codebase, name, cl, modifiers, type, tokenizer.pos())
         cl.addProperty(property)
     }
 
     private fun parseTypeParameterList(
-        codebase: TextCodebase,
-        tokenizer: Tokenizer
-    ): TypeParameterList {
+        tokenizer: Tokenizer,
+        enclosingTypeParameterScope: TypeParameterScope,
+    ): Pair<TypeParameterList, TypeParameterScope> {
         var token: String
         val start = tokenizer.offset() - 1
         var balance = 1
@@ -977,15 +1264,65 @@
                 balance--
             }
         }
-        val typeParameterList = tokenizer.getStringFromOffset(start)
-        return if (typeParameterList.isEmpty()) {
-            NONE
+        val typeParameterListString = tokenizer.getStringFromOffset(start)
+        return if (typeParameterListString.isEmpty()) {
+            Pair(TypeParameterList.NONE, enclosingTypeParameterScope)
         } else {
-            create(codebase, typeParameterList)
+            createTypeParameterList(enclosingTypeParameterScope, typeParameterListString)
         }
     }
 
     /**
+     * Creates a [TextTypeParameterList].
+     *
+     * The [typeParameterListString] should be the string representation of a list of type
+     * parameters, like "<A>" or "<A, B extends java.lang.String, C>".
+     *
+     * @return a [Pair] of [TypeParameterList] and [TypeParameterScope] that contains those type
+     *   parameters.
+     */
+    private fun createTypeParameterList(
+        enclosingTypeParameterScope: TypeParameterScope,
+        typeParameterListString: String
+    ): Pair<TypeParameterList, TypeParameterScope> {
+        // A type parameter list can contain cycles between its type parameters, e.g.
+        //     class Node<L extends Node<L, R>, R extends Node<L, R>>
+        // Parsing that requires a multi-stage approach.
+        // 1. Separate the list into a mapping from `TextTypeParameterItem` that have not yet
+        //    had their `bounds` property initialized to the bounds string list.
+        // 2. Create a nested scope of the enclosing scope which includes the type parameters.
+        //    That will allow references between them to be resolved.
+        // 3. Completing the initialization by converting each bounds string into a TypeItem.
+
+        // Split the type parameter list string into a list of strings, one for each type
+        // parameter.
+        val typeParameterStrings = TextTypeParser.typeParameterStrings(typeParameterListString)
+
+        // Creating a mapping from a `TextTypeParameterItem` to the bounds string list.
+        val itemToBoundsList =
+            typeParameterStrings.associateBy({ TextTypeParameterItem.create(codebase, it) }) {
+                extractTypeParameterBoundsStringList(it)
+            }
+
+        // Extract the `TextTypeParameterItem`s into a list and then use that to construct a
+        // scope that can be used to resolve the type parameters, including self references
+        // between the ones in this list.
+        val typeParameters = itemToBoundsList.keys.toList()
+        val scope = enclosingTypeParameterScope.nestedScope(typeParameters)
+
+        // Complete the initialization of the `TextTypeParameterItem`s by converting each bounds
+        // string into a `TypeItem`.
+        for ((typeParameterItem, boundsStringList) in itemToBoundsList) {
+            typeParameterItem.bounds =
+                boundsStringList.map {
+                    typeParser.obtainTypeFromString(it, scope) as BoundsTypeItem
+                }
+        }
+
+        return Pair(TextTypeParameterList.create(codebase, typeParameters), scope)
+    }
+
+    /**
      * Parses a list of parameters. Before calling, [tokenizer] should point to the token *before*
      * the opening `(` of the parameter list (the method starts by calling
      * [Tokenizer.requireToken]).
@@ -993,9 +1330,8 @@
      * When the method returns, [tokenizer] will point to the closing `)` of the parameter list.
      */
     private fun parseParameterList(
-        api: TextCodebase,
         tokenizer: Tokenizer,
-        typeParameters: List<TypeParameterItem>
+        typeParameterScope: TypeParameterScope
     ): List<TextParameterItem> {
         val parameters = mutableListOf<TextParameterItem>()
         var token: String = tokenizer.requireToken()
@@ -1023,7 +1359,7 @@
             // Metalava: including annotations in file now
             val annotations = getAnnotations(tokenizer, token)
             token = tokenizer.current
-            val modifiers = parseModifiers(api, tokenizer, token, null)
+            val modifiers = parseModifiers(tokenizer, token, null)
             token = tokenizer.current
 
             val type: TextTypeItem
@@ -1041,13 +1377,13 @@
                     }
                 token = tokenizer.requireToken()
                 // Token should now represent the type
-                type = parseType(api, tokenizer, token, typeParameters, annotations)
+                type = parseType(tokenizer, token, typeParameterScope, annotations)
                 // TODO(b/300081840): update nullability handling
                 modifiers.addAnnotations(annotations)
                 token = tokenizer.current
             } else {
                 // Java style: parse the type, then the public name if it has one.
-                type = parseType(api, tokenizer, token, typeParameters, annotations)
+                type = parseType(tokenizer, token, typeParameterScope, annotations)
                 modifiers.addAnnotations(annotations)
                 token = tokenizer.current
                 if (Tokenizer.isIdent(token) && token != "=") {
@@ -1119,7 +1455,7 @@
             }
             parameters.add(
                 TextParameterItem(
-                    api,
+                    codebase,
                     name,
                     publicName,
                     hasDefaultValue,
@@ -1176,7 +1512,7 @@
     /**
      * Parses a [TextTypeItem] from the [tokenizer], starting with the [startingToken] and ensuring
      * that the full type string is gathered, even when there are type-use annotations. Once the
-     * full type string is found, this parses the type in the context of the [typeParameters].
+     * full type string is found, this parses the type in the context of the [typeParameterScope].
      *
      * If the type string uses a Kotlin nullabililty suffix, this adds an annotation representing
      * that nullability to [annotations].
@@ -1191,10 +1527,9 @@
      * it if it contains an annotation. This is necessary to handle type strings like "Foo @A []".
      */
     private fun parseType(
-        api: TextCodebase,
         tokenizer: Tokenizer,
         startingToken: String,
-        typeParameters: List<TypeParameterItem>,
+        typeParameterScope: TypeParameterScope,
         annotations: MutableList<String>
     ): TextTypeItem {
         var prev = getAnnotationCompleteToken(tokenizer, startingToken)
@@ -1212,8 +1547,8 @@
             token = tokenizer.current
         }
 
-        val parsedType = api.typeResolver.obtainTypeFromString(type, typeParameters)
-        if (kotlinStyleNulls) {
+        val parsedType = typeParser.obtainTypeFromString(type, typeParameterScope)
+        if (typeParser.kotlinStyleNulls) {
             // Treat varargs as non-null for consistency with the psi model.
             if (parsedType is ArrayTypeItem && parsedType.isVarargs) {
                 mergeAnnotations(annotations, ANDROIDX_NONNULL)
@@ -1278,6 +1613,24 @@
     private fun qualifiedName(pkg: String, className: String): String {
         return "$pkg.$className"
     }
+
+    private val stats
+        get() =
+            Stats(
+                codebase.getPackages().allClasses().count(),
+                typeParser.requests,
+                typeParser.cacheSkip,
+                typeParser.cacheHit,
+                typeParser.cacheSize,
+            )
+
+    data class Stats(
+        val totalClasses: Int,
+        val typeCacheRequests: Int,
+        val typeCacheSkip: Int,
+        val typeCacheHit: Int,
+        val typeCacheSize: Int,
+    )
 }
 
 /**
@@ -1286,30 +1639,20 @@
  * This is provided by [ApiFile] which tracks the names of interfaces and super classes that each
  * class implements/extends respectively before they are resolved.
  */
-interface ResolverContext {
+internal interface ResolverContext {
     /**
-     * Get the names of the interfaces implemented by the supplied class, returns null if there are
-     * no interfaces.
+     * Get the string representation of the super class type extended by the supplied class, returns
+     * null if there was no specified super class type.
      */
-    fun namesOfInterfaces(cl: TextClassItem): List<String>?
-
-    /**
-     * Get the name of the super class extended by the supplied class, returns null if there is no
-     * super class.
-     */
-    fun nameOfSuperClass(cl: TextClassItem): String?
-
-    /**
-     * The optional [ClassResolver] that is used to resolve unknown classes within the
-     * [TextCodebase].
-     */
-    val classResolver: ClassResolver?
+    fun superClassTypeString(cl: ClassItem): String?
 }
 
 /** Resolves any references in the codebase, e.g. to superclasses, interfaces, etc. */
-class ReferenceResolver(
+internal class ReferenceResolver(
     private val context: ResolverContext,
     private val codebase: TextCodebase,
+    private val typeParser: TextTypeParser,
+    private val classScopeProvider: (ClassItem) -> TypeParameterScope,
 ) {
     /**
      * A list of all the classes in the text codebase.
@@ -1319,51 +1662,21 @@
      */
     private val classes = codebase.mAllClasses.values.toList()
 
-    /**
-     * A list of all the packages in the text codebase.
-     *
-     * This takes a copy of the `values` collection rather than use it correctly to avoid
-     * [ConcurrentModificationException].
-     */
-    private val packages = codebase.mPackages.values.toList()
-
     companion object {
-        fun resolveReferences(context: ResolverContext, codebase: TextCodebase) {
-            val resolver = ReferenceResolver(context, codebase)
+        fun resolveReferences(
+            context: ResolverContext,
+            codebase: TextCodebase,
+            typeParser: TextTypeParser,
+            classScopeProvider: (ClassItem) -> TypeParameterScope = { TypeParameterScope.empty },
+        ) {
+            val resolver = ReferenceResolver(context, codebase, typeParser, classScopeProvider)
             resolver.resolveReferences()
         }
     }
 
     fun resolveReferences() {
         resolveSuperclasses()
-        resolveInterfaces()
         resolveThrowsClasses()
-        resolveInnerClasses()
-    }
-
-    /**
-     * Gets an existing, or creates a new [ClassItem].
-     *
-     * @param name the name of the class, may include generics.
-     * @param isInterface true if the class must be an interface, i.e. is referenced from an
-     *   `implements` list (or Kotlin equivalent).
-     * @param mustBeFromThisCodebase true if the class must be from the same codebase as this class
-     *   is currently resolving.
-     */
-    private fun getOrCreateClass(
-        name: String,
-        isInterface: Boolean = false,
-        mustBeFromThisCodebase: Boolean = false
-    ): ClassItem {
-        return if (mustBeFromThisCodebase) {
-            codebase.getOrCreateClass(name, isInterface = isInterface, classResolver = null)
-        } else {
-            codebase.getOrCreateClass(
-                name,
-                isInterface = isInterface,
-                classResolver = context.classResolver
-            )
-        }
     }
 
     private fun resolveSuperclasses() {
@@ -1372,120 +1685,84 @@
             if (cl.isJavaLangObject() || cl.isInterface()) {
                 continue
             }
-            var scName: String? = context.nameOfSuperClass(cl)
-            if (scName == null) {
-                scName =
-                    when {
-                        cl.isEnum() -> JAVA_LANG_ENUM
-                        cl.isAnnotationType() -> JAVA_LANG_ANNOTATION
-                        // Interfaces do not extend java.lang.Object so drop out before the else
-                        // clause.
-                        cl.isInterface() -> return
-                        else -> {
-                            val existing = cl.superClassType()?.toTypeString()
-                            existing ?: JAVA_LANG_OBJECT
-                        }
+            val superClassTypeString: String =
+                context.superClassTypeString(cl)
+                    ?: when (cl.classKind) {
+                        ClassKind.ENUM -> JAVA_LANG_ENUM
+                        // Interfaces and annotations do not have super classes so drop out before
+                        // the else clause.
+                        ClassKind.ANNOTATION_TYPE,
+                        ClassKind.INTERFACE -> continue
+                        else -> JAVA_LANG_OBJECT
                     }
-            }
 
-            val superclass = getOrCreateClass(scName)
-            cl.setSuperClass(
-                superclass,
-                codebase.typeResolver.obtainTypeFromString(
-                    scName,
-                    cl.typeParameterList.typeParameters()
-                )
-            )
-        }
-    }
+            val superClassType =
+                typeParser.getSuperType(superClassTypeString, classScopeProvider(cl))
+            cl.setSuperClassType(superClassType)
 
-    private fun resolveInterfaces() {
-        for (cl in classes) {
-            val interfaces = context.namesOfInterfaces(cl) ?: continue
-            for (interfaceName in interfaces) {
-                getOrCreateClass(interfaceName, isInterface = true)
-                cl.addInterface(
-                    codebase.typeResolver.obtainTypeFromString(
-                        interfaceName,
-                        cl.typeParameterList.typeParameters()
-                    )
-                )
-            }
+            // Resolve super class types. This is needed because otherwise code that manipulates
+            // the codebase while visiting the codebase can cause concurrent modification
+            // exceptions.
+            // TODO(b/323516595): Find a better way.
+            cl.superClass()
         }
     }
 
     private fun resolveThrowsClasses() {
         for (cl in classes) {
+            val classTypeParameterScope = classScopeProvider(cl)
             for (methodItem in cl.constructors()) {
-                resolveThrowsClasses(methodItem)
+                resolveThrowsClasses(classTypeParameterScope, methodItem)
             }
             for (methodItem in cl.methods()) {
-                resolveThrowsClasses(methodItem)
+                resolveThrowsClasses(classTypeParameterScope, methodItem)
             }
         }
     }
 
-    private fun resolveThrowsClasses(methodItem: MethodItem) {
+    private fun resolveThrowsClasses(
+        classTypeParameterScope: TypeParameterScope,
+        methodItem: MethodItem
+    ) {
         val methodInfo = methodItem as TextMethodItem
         val names = methodInfo.throwsTypeNames()
         if (names.isNotEmpty()) {
-            val result = ArrayList<ClassItem>()
-            for (exception in names) {
-                var exceptionClass: ClassItem? = codebase.mAllClasses[exception]
-                if (exceptionClass == null) {
-                    // Exception not provided by this codebase. Either try and retrieve it from a
-                    // base codebase or create a stub.
-                    exceptionClass = getOrCreateClass(exception)
-
-                    // A class retrieved from another codebase is assumed to have been fully
-                    // resolved by the codebase. However, a stub that has just been created will
-                    // need some additional work. A stub can be differentiated from a ClassItem
-                    // retrieved from another codebase because it belongs to this codebase and is
-                    // a TextClassItem.
-                    if (exceptionClass.codebase == codebase && exceptionClass is TextClassItem) {
-                        // An exception class needs to extend Throwable, unless it is Throwable in
-                        // which case it does not need modifying.
-                        if (exception != JAVA_LANG_THROWABLE) {
-                            val throwableClass = getOrCreateClass(JAVA_LANG_THROWABLE)
-                            exceptionClass.setSuperClass(throwableClass, throwableClass.toType())
+            val typeParameterScope =
+                classTypeParameterScope.nestedScope(methodItem.typeParameterList().typeParameters())
+            val throwsList =
+                names.map { exception ->
+                    // Search in this codebase, then possibly check for a type parameter, if not
+                    // found then fall back to searching in a base codebase and finally creating a
+                    // stub.
+                    codebase.findClassInCodebase(exception)?.let { ThrowableType.ofClass(it) }
+                        ?: typeParameterScope.findTypeParameter(exception)?.let {
+                            ThrowableType.ofTypeParameter(it)
                         }
-                    }
+                            ?: getOrCreateThrowableClass(exception)
                 }
-                result.add(exceptionClass)
-            }
-            methodInfo.setThrowsList(result)
+            methodInfo.setThrowsList(throwsList)
         }
     }
 
-    private fun resolveInnerClasses() {
-        for (pkg in packages) {
-            // make copy: we'll be removing non-top level classes during iteration
-            val classes = ArrayList(pkg.classList())
-            for (cls in classes) {
-                // External classes are already resolved.
-                if (cls.codebase != codebase) continue
-                val cl = cls as TextClassItem
-                val name = cl.name
-                var index = name.lastIndexOf('.')
-                if (index != -1) {
-                    cl.name = name.substring(index + 1)
-                    val qualifiedName = cl.qualifiedName
-                    index = qualifiedName.lastIndexOf('.')
-                    assert(index != -1) { qualifiedName }
-                    val outerClassName = qualifiedName.substring(0, index)
-                    // If the outer class doesn't exist in the text codebase, it should not be
-                    // resolved through the classpath--if it did exist there, this inner class
-                    // would be overridden by the version from the classpath.
-                    val outerClass = getOrCreateClass(outerClassName, mustBeFromThisCodebase = true)
-                    cl.containingClass = outerClass
-                    outerClass.addInnerClass(cl)
-                }
+    private fun getOrCreateThrowableClass(exception: String): ThrowableType {
+        // Exception not provided by this codebase. Either try and retrieve it from a base codebase
+        // or create a stub.
+        val exceptionClass = codebase.getOrCreateClass(exception)
+
+        // A class retrieved from another codebase is assumed to have been fully resolved by the
+        // codebase. However, a stub that has just been created will need some additional work. A
+        // stub can be differentiated from a ClassItem retrieved from another codebase because it
+        // belongs to this codebase and is a TextClassItem.
+        if (exceptionClass.codebase == codebase && exceptionClass is TextClassItem) {
+            // An exception class needs to extend Throwable, unless it is Throwable in
+            // which case it does not need modifying.
+            if (exception != JAVA_LANG_THROWABLE) {
+                val throwableClass = codebase.getOrCreateClass(JAVA_LANG_THROWABLE)
+                exceptionClass.setSuperClassType(throwableClass.type())
             }
         }
 
-        for (pkg in packages) {
-            pkg.pruneClassList()
-        }
+        return ThrowableType.ofClass(exceptionClass)
     }
 }
 
@@ -1505,27 +1782,3 @@
         addAnnotation(item)
     }
 }
-
-/**
- * Checks if the [cls] from different signature file can be merged with this [TextClassItem]. For
- * instance, `current.txt` and `system-current.txt` may contain equal class definitions with
- * different class methods. This method is used to determine if the two [TextClassItem]s can be
- * safely merged in such scenarios.
- *
- * @param cls [TextClassItem] to be checked if it is compatible with [this] and can be merged
- * @return a Boolean value representing if [cls] is compatible with [this]
- */
-private fun TextClassItem.isCompatible(cls: TextClassItem): Boolean {
-    if (this === cls) {
-        return true
-    }
-    if (fullName() != cls.fullName()) {
-        return false
-    }
-
-    return modifiers == cls.modifiers &&
-        isInterface() == cls.isInterface() &&
-        isEnum() == cls.isEnum() &&
-        isAnnotation == cls.isAnnotation &&
-        allInterfaces().toSet() == cls.allInterfaces().toSet()
-}
diff --git a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/ClassCharacteristics.kt b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/ClassCharacteristics.kt
new file mode 100644
index 0000000..36a18e2
--- /dev/null
+++ b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/ClassCharacteristics.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.model.text
+
+import com.android.tools.metalava.model.ClassKind
+import com.android.tools.metalava.model.ModifierList
+
+/**
+ * Characteristics of a class apart from its members.
+ *
+ * This is basically everything that could appear on the line defining the class in the API
+ * signature file.
+ */
+internal data class ClassCharacteristics(
+    /** The position of the class definition within the API signature file. */
+    val position: SourcePositionInfo,
+
+    /** Name including package and full name. */
+    val qualifiedName: String,
+
+    /**
+     * Full name, this is in addition to [qualifiedName] as it is possible for two classed to have
+     * the same qualified name but different full names. e.g. `a.b.c.D.E` in package `a.b.c` has a
+     * full name of `D.E` but in a package `a.b` has a full name of `c.D.E`. While those names would
+     * break naming conventions and so would be unlikely they are possible.
+     */
+    val fullName: String,
+
+    /** The kind of the class. */
+    val classKind: ClassKind,
+
+    /** The modifiers. */
+    val modifiers: ModifierList,
+
+    /** The super class type string. */
+    val superClassTypeString: String?,
+// TODO(b/323168612): Add interface type strings.
+) {
+    /**
+     * Checks if the [cls] from different signature file can be merged with this [TextClassItem].
+     * For instance, `current.txt` and `system-current.txt` may contain equal class definitions with
+     * different class methods. This method is used to determine if the two [TextClassItem]s can be
+     * safely merged in such scenarios.
+     *
+     * @param cls [TextClassItem] to be checked if it is compatible with [this] and can be merged
+     * @return a Boolean value representing if [cls] is compatible with [this]
+     */
+    fun isCompatible(other: ClassCharacteristics): Boolean {
+        // TODO(b/323168612): Check super interface types and super class type of the two
+        // TextClassItem
+        return fullName == other.fullName &&
+            classKind == other.classKind &&
+            modifiers == other.modifiers
+    }
+
+    companion object {
+        fun of(classItem: TextClassItem): ClassCharacteristics =
+            ClassCharacteristics(
+                position = classItem.position,
+                qualifiedName = classItem.qualifiedName,
+                fullName = classItem.fullName(),
+                classKind = classItem.classKind,
+                modifiers = classItem.modifiers,
+                superClassTypeString = classItem.superClassType()?.toTypeString(),
+            )
+    }
+}
diff --git a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt
index 7742f22..43e52e1 100644
--- a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt
+++ b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt
@@ -18,6 +18,8 @@
 
 import com.android.tools.metalava.model.AnnotationRetention
 import com.android.tools.metalava.model.ClassItem
+import com.android.tools.metalava.model.ClassKind
+import com.android.tools.metalava.model.ClassTypeItem
 import com.android.tools.metalava.model.ConstructorItem
 import com.android.tools.metalava.model.DefaultModifierList
 import com.android.tools.metalava.model.FieldItem
@@ -26,42 +28,28 @@
 import com.android.tools.metalava.model.PackageItem
 import com.android.tools.metalava.model.PropertyItem
 import com.android.tools.metalava.model.TypeItem
-import com.android.tools.metalava.model.TypeParameterItem
 import com.android.tools.metalava.model.TypeParameterList
-import com.android.tools.metalava.model.TypeParameterListOwner
 import java.util.function.Predicate
 
-open class TextClassItem(
+internal open class TextClassItem(
     override val codebase: TextCodebase,
     position: SourcePositionInfo = SourcePositionInfo.UNKNOWN,
     modifiers: DefaultModifierList,
-    private var isInterface: Boolean = false,
-    private var isEnum: Boolean = false,
-    internal var isAnnotation: Boolean = false,
+    override val classKind: ClassKind = ClassKind.CLASS,
     val qualifiedName: String = "",
-    val qualifiedTypeName: String = qualifiedName,
-    var name: String = qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1),
+    var simpleName: String = qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1),
+    val fullName: String = simpleName,
     val annotations: List<String>? = null,
     val typeParameterList: TypeParameterList = TypeParameterList.NONE
-) :
-    TextItem(codebase = codebase, position = position, modifiers = modifiers),
-    ClassItem,
-    TypeParameterListOwner {
-
-    init {
-        @Suppress("LeakingThis") modifiers.setOwner(this)
-        if (typeParameterList is TextTypeParameterList) {
-            @Suppress("LeakingThis") typeParameterList.setOwner(this)
-        }
-    }
-
-    override val isTypeParameter: Boolean = false
+) : TextItem(codebase = codebase, position = position, modifiers = modifiers), ClassItem {
 
     override var artifact: String? = null
 
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
-        if (other !is ClassItem) return false
+        if (javaClass != other?.javaClass) return false
+
+        other as TextClassItem
 
         return qualifiedName == other.qualifiedName()
     }
@@ -70,12 +58,12 @@
         return qualifiedName.hashCode()
     }
 
-    override fun interfaceTypes(): List<TypeItem> = interfaceTypes
+    override fun interfaceTypes(): List<ClassTypeItem> = interfaceTypes
 
     override fun allInterfaces(): Sequence<ClassItem> {
         return sequenceOf(
                 // Add this if and only if it is an interface.
-                if (isInterface) sequenceOf(this) else emptySequence(),
+                if (classKind == ClassKind.INTERFACE) sequenceOf(this) else emptySequence(),
                 interfaceTypes.asSequence().map { it.asClass() }.filterNotNull(),
             )
             .flatten()
@@ -93,12 +81,6 @@
         return false
     }
 
-    override fun isInterface(): Boolean = isInterface
-
-    override fun isAnnotationType(): Boolean = isAnnotation
-
-    override fun isEnum(): Boolean = isEnum
-
     var containingClass: ClassItem? = null
 
     override fun containingClass(): ClassItem? = containingClass
@@ -109,14 +91,6 @@
         this.containingPackage = containingPackage
     }
 
-    fun setIsAnnotationType(isAnnotation: Boolean) {
-        this.isAnnotation = isAnnotation
-    }
-
-    fun setIsEnum(isEnum: Boolean) {
-        this.isEnum = isEnum
-    }
-
     override fun containingPackage(): PackageItem =
         containingClass?.containingPackage() ?: containingPackage ?: error(this)
 
@@ -124,48 +98,39 @@
 
     override fun typeParameterList(): TypeParameterList = typeParameterList
 
-    override fun typeParameterListOwnerParent(): TypeParameterListOwner? {
-        return containingClass as? TypeParameterListOwner
-    }
+    private var superClassType: ClassTypeItem? = null
 
-    override fun resolveParameter(variable: String): TypeParameterItem? {
-        if (hasTypeVariables()) {
-            for (t in typeParameterList().typeParameters()) {
-                if (t.simpleName() == variable) {
-                    return t
-                }
-            }
-        }
+    override fun superClass(): ClassItem? = superClassType?.asClass()
 
-        return null
-    }
+    override fun superClassType(): ClassTypeItem? = superClassType
 
-    private var superClass: ClassItem? = null
-    private var superClassType: TypeItem? = null
-
-    override fun superClass(): ClassItem? = superClass
-
-    override fun superClassType(): TypeItem? = superClassType
-
-    internal fun setSuperClass(superClass: ClassItem?, superClassType: TypeItem?) {
-        this.superClass = superClass
+    internal fun setSuperClassType(superClassType: ClassTypeItem?) {
         this.superClassType = superClassType
     }
 
-    override fun setInterfaceTypes(interfaceTypes: List<TypeItem>) {
-        this.interfaceTypes = interfaceTypes.toMutableList()
+    override fun setInterfaceTypes(interfaceTypes: List<ClassTypeItem>) {
+        this.interfaceTypes = interfaceTypes
     }
 
-    private var typeInfo: TextTypeItem? = null
+    private var typeInfo: TextClassTypeItem? = null
 
-    override fun toType(): TextTypeItem {
+    override fun type(): TextClassTypeItem {
         if (typeInfo == null) {
-            typeInfo = codebase.typeResolver.obtainTypeFromClass(this)
+            val params = typeParameterList.typeParameters().map { it.type() }
+            // Create a [TextTypeItem] representing the type of this class.
+            typeInfo =
+                TextClassTypeItem(
+                    codebase,
+                    qualifiedName,
+                    params,
+                    containingClass()?.type(),
+                    codebase.emptyTypeModifiers,
+                )
         }
         return typeInfo!!
     }
 
-    private var interfaceTypes = mutableListOf<TypeItem>()
+    private var interfaceTypes = emptyList<ClassTypeItem>()
     private val constructors = mutableListOf<ConstructorItem>()
     private val methods = mutableListOf<MethodItem>()
     private val fields = mutableListOf<FieldItem>()
@@ -179,10 +144,6 @@
 
     override fun properties(): List<PropertyItem> = properties
 
-    fun addInterface(itf: TypeItem) {
-        interfaceTypes.add(itf)
-    }
-
     fun addConstructor(constructor: TextConstructorItem) {
         constructors += constructor
     }
@@ -231,9 +192,7 @@
         return retention!!
     }
 
-    private var fullName: String = name
-
-    override fun simpleName(): String = name.substring(name.lastIndexOf('.') + 1)
+    override fun simpleName(): String = simpleName
 
     override fun fullName(): String = fullName
 
@@ -253,26 +212,17 @@
     companion object {
         internal fun createStubClass(
             codebase: TextCodebase,
-            name: String,
+            qualifiedName: String,
             isInterface: Boolean
         ): TextClassItem {
-            val index = if (name.endsWith(">")) name.indexOf('<') else -1
-            val qualifiedName = if (index == -1) name else name.substring(0, index)
-            val typeParameterList =
-                if (index == -1) {
-                    TypeParameterList.NONE
-                } else {
-                    TextTypeParameterList.create(codebase, name.substring(index))
-                }
             val fullName = getFullName(qualifiedName)
             val cls =
                 TextClassItem(
                     codebase = codebase,
-                    name = fullName,
                     qualifiedName = qualifiedName,
-                    isInterface = isInterface,
+                    fullName = fullName,
+                    classKind = if (isInterface) ClassKind.INTERFACE else ClassKind.CLASS,
                     modifiers = DefaultModifierList(codebase, DefaultModifierList.PUBLIC),
-                    typeParameterList = typeParameterList
                 )
             cls.emit = false // it's a stub
 
diff --git a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextCodebase.kt b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextCodebase.kt
index e993075..1d0097f 100644
--- a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextCodebase.kt
+++ b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextCodebase.kt
@@ -33,16 +33,20 @@
 // Copy of ApiInfo in doclava1 (converted to Kotlin + some cleanup to make it work with metalava's
 // data structures.
 // (Converted to Kotlin such that I can inherit behavior via interfaces, in particular Codebase.)
-class TextCodebase(
+internal class TextCodebase(
     location: File,
     annotationManager: AnnotationManager,
+    private val classResolver: ClassResolver?,
 ) : DefaultCodebase(location, "Codebase", true, annotationManager) {
     internal val mPackages = HashMap<String, TextPackageItem>(300)
     internal val mAllClasses = HashMap<String, TextClassItem>(30000)
 
     private val externalClasses = HashMap<String, ClassItem>()
 
-    internal val typeResolver = TextTypeParser(this)
+    /**
+     * A set of empty [TextTypeModifiers] owned by, and reused by items within, this [TextCodebase].
+     */
+    internal val emptyTypeModifiers = TextTypeModifiers.create(this, emptyList(), null)
 
     override fun trustedApi(): Boolean = true
 
@@ -56,9 +60,12 @@
         return mPackages.size
     }
 
-    override fun findClass(className: String): TextClassItem? {
-        return mAllClasses[className]
-    }
+    /** Find a class in this codebase, i.e. not classes loaded from the [classResolver]. */
+    fun findClassInCodebase(className: String) = mAllClasses[className]
+
+    override fun findClass(className: String) = mAllClasses[className] ?: externalClasses[className]
+
+    override fun resolveClass(className: String) = getOrCreateClass(className)
 
     override fun supportsDocumentation(): Boolean = false
 
@@ -77,44 +84,61 @@
     }
 
     /**
+     * Gets an existing, or creates a new [ClassItem].
+     *
      * Tries to find [name] in [mAllClasses]. If not found, then if a [classResolver] is provided it
      * will invoke that and return the [ClassItem] it returns if any. Otherwise, it will create an
      * empty stub class (or interface, if [isInterface] is true).
      *
      * Initializes outer classes and packages for the created class as needed.
+     *
+     * @param name the name of the class.
+     * @param isInterface true if the class must be an interface, i.e. is referenced from an
+     *   `implements` list (or Kotlin equivalent).
+     * @param isOuterClass if `true` then this is searching for an outer class of a class in this
+     *   codebase, in which case this must only search classes in this codebase, otherwise it can
+     *   search for external classes too.
      */
     fun getOrCreateClass(
         name: String,
         isInterface: Boolean = false,
-        classResolver: ClassResolver? = null,
+        isOuterClass: Boolean = false,
     ): ClassItem {
-        val erased = TextTypeItem.eraseTypeArguments(name)
-        val cls = mAllClasses[erased] ?: externalClasses[erased]
-        if (cls != null) {
-            return cls
+        // Check this codebase first, if found then return it.
+        mAllClasses[name]?.let { found ->
+            return found
         }
 
-        if (classResolver != null) {
-            val classItem = classResolver.resolveClass(erased)
+        // Only check for external classes if this is not searching for an outer class and there is
+        // a class resolver that will populate the external classes.
+        if (!isOuterClass && classResolver != null) {
+            // Check to see whether the class has already been retrieved from the resolver. If it
+            // has then return it.
+            externalClasses[name]?.let { found ->
+                return found
+            }
+
+            // Else try and resolve the class.
+            val classItem = classResolver.resolveClass(name)
             if (classItem != null) {
                 // Save the class item, so it can be retrieved the next time this is loaded. This is
                 // needed because otherwise TextTypeItem.asClass would not work properly.
-                externalClasses[erased] = classItem
+                externalClasses[name] = classItem
                 return classItem
             }
         }
 
         val stubClass = TextClassItem.createStubClass(this, name, isInterface)
-        mAllClasses[erased] = stubClass
+        mAllClasses[name] = stubClass
         stubClass.emit = false
 
         val fullName = stubClass.fullName()
         if (fullName.contains('.')) {
             // We created a new inner class stub. We need to fully initialize it with outer classes,
             // themselves possibly stubs
-            val outerName = erased.substring(0, erased.lastIndexOf('.'))
+            val outerName = name.substring(0, name.lastIndexOf('.'))
             // Pass classResolver = null, so it only looks in this codebase for the outer class.
-            val outerClass = getOrCreateClass(outerName, isInterface = false, classResolver = null)
+            val outerClass = getOrCreateClass(outerName, isOuterClass = true)
 
             // It makes no sense for a Foo to come from one codebase and Foo.Bar to come from
             // another.
@@ -129,8 +153,8 @@
             outerClass.addInnerClass(stubClass)
         } else {
             // Add to package
-            val endIndex = erased.lastIndexOf('.')
-            val pkgPath = if (endIndex != -1) erased.substring(0, endIndex) else ""
+            val endIndex = name.lastIndexOf('.')
+            val pkgPath = if (endIndex != -1) name.substring(0, endIndex) else ""
             val pkg =
                 findPackage(pkgPath)
                     ?: run {
diff --git a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextCodebaseBuilder.kt b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextCodebaseBuilder.kt
new file mode 100644
index 0000000..c204107
--- /dev/null
+++ b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextCodebaseBuilder.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.model.text
+
+import com.android.tools.metalava.model.AnnotationManager
+import com.android.tools.metalava.model.ClassItem
+import com.android.tools.metalava.model.Codebase
+import com.android.tools.metalava.model.ConstructorItem
+import com.android.tools.metalava.model.DefaultModifierList
+import com.android.tools.metalava.model.FieldItem
+import com.android.tools.metalava.model.MethodItem
+import com.android.tools.metalava.model.PackageItem
+import com.android.tools.metalava.model.PropertyItem
+import java.io.File
+
+/**
+ * Supports building a [TextCodebase] that is a subset of another [TextCodebase].
+ *
+ * The purposely uses generic model classes in the API and down casts any items provided to the
+ * appropriate text model item. That is to avoid external dependencies on the text model item
+ * implementation classes.
+ */
+class TextCodebaseBuilder private constructor(private val codebase: TextCodebase) {
+
+    companion object {
+        fun build(
+            location: File,
+            annotationManager: AnnotationManager,
+            block: TextCodebaseBuilder.() -> Unit
+        ): Codebase {
+            val codebase =
+                TextCodebase(
+                    location = location,
+                    annotationManager = annotationManager,
+                    classResolver = null,
+                )
+            val builder = TextCodebaseBuilder(codebase)
+            builder.block()
+
+            // As the codebase has not been created by the parser there is no parser provided
+            // context to use so just use an empty context.
+            val context =
+                object : ResolverContext {
+
+                    override fun superClassTypeString(cl: ClassItem): String? = null
+                }
+
+            // All this actually does is add in an appropriate super class depending on the class
+            // type.
+            ReferenceResolver.resolveReferences(context, codebase, TextTypeParser(codebase))
+
+            return codebase
+        }
+    }
+
+    var description by codebase::description
+
+    private fun getOrAddPackage(pkgName: String): TextPackageItem {
+        val pkg = codebase.findPackage(pkgName)
+        if (pkg != null) {
+            return pkg
+        }
+        val newPkg =
+            TextPackageItem(
+                codebase,
+                pkgName,
+                DefaultModifierList(codebase, DefaultModifierList.PUBLIC),
+                SourcePositionInfo.UNKNOWN
+            )
+        codebase.addPackage(newPkg)
+        return newPkg
+    }
+
+    fun addPackage(pkg: PackageItem) {
+        codebase.addPackage(pkg as TextPackageItem)
+    }
+
+    fun addClass(cls: ClassItem) {
+        val pkg = getOrAddPackage(cls.containingPackage().qualifiedName())
+        pkg.addClass(cls as TextClassItem)
+    }
+
+    fun addConstructor(ctor: ConstructorItem) {
+        val cls = getOrAddClass(ctor.containingClass())
+        cls.addConstructor(ctor as TextConstructorItem)
+    }
+
+    fun addMethod(method: MethodItem) {
+        val cls = getOrAddClass(method.containingClass())
+        cls.addMethod(method as TextMethodItem)
+    }
+
+    fun addField(field: FieldItem) {
+        val cls = getOrAddClass(field.containingClass())
+        cls.addField(field as TextFieldItem)
+    }
+
+    fun addProperty(property: PropertyItem) {
+        val cls = getOrAddClass(property.containingClass())
+        cls.addProperty(property as TextPropertyItem)
+    }
+
+    private fun getOrAddClass(fullClass: ClassItem): TextClassItem {
+        val cls = codebase.findClassInCodebase(fullClass.qualifiedName())
+        if (cls != null) {
+            return cls
+        }
+        val textClass = fullClass as TextClassItem
+        val newClass =
+            TextClassItem(
+                codebase = codebase,
+                position = SourcePositionInfo.UNKNOWN,
+                modifiers = textClass.modifiers,
+                classKind = textClass.classKind,
+                qualifiedName = textClass.qualifiedName,
+                simpleName = textClass.simpleName,
+                fullName = textClass.fullName,
+                annotations = textClass.annotations,
+                typeParameterList = textClass.typeParameterList,
+            )
+        val pkg = getOrAddPackage(fullClass.containingPackage().qualifiedName())
+        pkg.addClass(newClass)
+        newClass.setContainingPackage(pkg)
+        codebase.registerClass(newClass)
+        return newClass
+    }
+}
diff --git a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt
index 454d9f2..049759d 100644
--- a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt
+++ b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt
@@ -19,7 +19,7 @@
 import com.android.tools.metalava.model.ConstructorItem
 import com.android.tools.metalava.model.DefaultModifierList
 
-class TextConstructorItem(
+internal class TextConstructorItem(
     codebase: TextCodebase,
     name: String,
     containingClass: TextClassItem,
@@ -41,7 +41,7 @@
             containingClass: TextClassItem,
             position: SourcePositionInfo,
         ): TextConstructorItem {
-            val name = containingClass.name
+            val name = containingClass.simpleName
             // The default constructor is package private because while in Java a class without
             // a constructor has a default public constructor in a signature file a class
             // without a constructor has no public constructors.
@@ -53,7 +53,7 @@
                     name = name,
                     containingClass = containingClass,
                     modifiers = modifiers,
-                    returnType = containingClass.toType(),
+                    returnType = containingClass.type(),
                     parameters = emptyList(),
                     position = position,
                 )
diff --git a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextFieldItem.kt b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextFieldItem.kt
index 469fccb..ee34ad5 100644
--- a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextFieldItem.kt
+++ b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextFieldItem.kt
@@ -21,7 +21,7 @@
 import com.android.tools.metalava.model.FieldItem
 import com.android.tools.metalava.model.TypeItem
 
-class TextFieldItem(
+internal class TextFieldItem(
     codebase: TextCodebase,
     name: String,
     containingClass: TextClassItem,
diff --git a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextItem.kt b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextItem.kt
index 475298e..7f2088a 100644
--- a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextItem.kt
+++ b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextItem.kt
@@ -22,7 +22,7 @@
 import com.android.tools.metalava.model.MutableModifierList
 import java.nio.file.Path
 
-abstract class TextItem(
+internal abstract class TextItem(
     override val codebase: TextCodebase,
     internal val position: SourcePositionInfo,
     override var docOnly: Boolean = false,
diff --git a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextMemberItem.kt b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextMemberItem.kt
index de8a55d..73dd50c 100644
--- a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextMemberItem.kt
+++ b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextMemberItem.kt
@@ -20,7 +20,7 @@
 import com.android.tools.metalava.model.DefaultModifierList
 import com.android.tools.metalava.model.MemberItem
 
-abstract class TextMemberItem(
+internal abstract class TextMemberItem(
     codebase: TextCodebase,
     private val name: String,
     private val containingClass: ClassItem,
diff --git a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt
index 1d90027..8f7024f 100644
--- a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt
+++ b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt
@@ -21,14 +21,13 @@
 import com.android.tools.metalava.model.Item
 import com.android.tools.metalava.model.MethodItem
 import com.android.tools.metalava.model.ParameterItem
+import com.android.tools.metalava.model.ThrowableType
 import com.android.tools.metalava.model.TypeItem
-import com.android.tools.metalava.model.TypeParameterItem
 import com.android.tools.metalava.model.TypeParameterList
-import com.android.tools.metalava.model.TypeParameterListOwner
 import com.android.tools.metalava.model.computeSuperMethods
 import java.util.function.Predicate
 
-open class TextMethodItem(
+internal open class TextMethodItem(
     codebase: TextCodebase,
     name: String,
     containingClass: ClassItem,
@@ -36,10 +35,7 @@
     private val returnType: TypeItem,
     private val parameters: List<TextParameterItem>,
     position: SourcePositionInfo
-) :
-    TextMemberItem(codebase, name, containingClass, position, modifiers = modifiers),
-    MethodItem,
-    TypeParameterListOwner {
+) : TextMemberItem(codebase, name, containingClass, position, modifiers = modifiers), MethodItem {
     init {
         @Suppress("LeakingThis") modifiers.setOwner(this)
         parameters.forEach { it.containingMethod = this }
@@ -113,20 +109,6 @@
 
     override fun typeParameterList(): TypeParameterList = typeParameterList
 
-    override fun typeParameterListOwnerParent(): TypeParameterListOwner? {
-        return containingClass() as TextClassItem?
-    }
-
-    override fun resolveParameter(variable: String): TypeParameterItem? {
-        for (t in typeParameterList.typeParameters()) {
-            if (t.simpleName() == variable) {
-                return t
-            }
-        }
-
-        return (containingClass() as TextClassItem).resolveParameter(variable)
-    }
-
     override fun duplicate(targetContainingClass: ClassItem): MethodItem {
         val typeVariableMap = targetContainingClass.mapTypeVariables(containingClass())
         val duplicated =
@@ -165,16 +147,16 @@
         get() = isEnumSyntheticMethod()
 
     private val throwsTypes = mutableListOf<String>()
-    private var throwsClasses: List<ClassItem>? = null
+    private var throwsClasses: List<ThrowableType>? = null
 
     fun throwsTypeNames(): List<String> {
         return throwsTypes
     }
 
-    override fun throwsTypes(): List<ClassItem> =
+    override fun throwsTypes(): List<ThrowableType> =
         if (throwsClasses == null) emptyList() else throwsClasses!!
 
-    fun setThrowsList(throwsClasses: List<ClassItem>) {
+    fun setThrowsList(throwsClasses: List<ThrowableType>) {
         this.throwsClasses = throwsClasses
     }
 
@@ -184,10 +166,6 @@
         throwsTypes += throwsType
     }
 
-    private val varargs: Boolean = parameters.any { it.isVarArgs() }
-
-    fun isVarArg(): Boolean = varargs
-
     override fun isExtensionMethod(): Boolean = codebase.unsupported()
 
     override var inheritedFrom: ClassItem? = null
diff --git a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt
index 4a3c694..dbe2f01 100644
--- a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt
+++ b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt
@@ -20,7 +20,7 @@
 import com.android.tools.metalava.model.DefaultModifierList
 import com.android.tools.metalava.model.PackageItem
 
-class TextPackageItem(
+internal class TextPackageItem(
     codebase: TextCodebase,
     private val name: String,
     modifiers: DefaultModifierList,
@@ -45,18 +45,6 @@
         classesNames.add(classFullName)
     }
 
-    internal fun pruneClassList() {
-        val iterator = classes.listIterator()
-        while (iterator.hasNext()) {
-            val cls = iterator.next()
-            if (cls.isInnerClass()) {
-                iterator.remove()
-            }
-        }
-    }
-
-    internal fun classList(): List<ClassItem> = classes
-
     override fun topLevelClasses(): Sequence<ClassItem> = classes.asSequence()
 
     override fun qualifiedName(): String = name
diff --git a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextParameterItem.kt b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextParameterItem.kt
index 9f8e52e..ab29187 100644
--- a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextParameterItem.kt
+++ b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextParameterItem.kt
@@ -20,10 +20,11 @@
 import com.android.tools.metalava.model.MethodItem
 import com.android.tools.metalava.model.ParameterItem
 import com.android.tools.metalava.model.TypeItem
+import com.android.tools.metalava.model.TypeParameterBindings
 
 const val UNKNOWN_DEFAULT_VALUE = "__unknown_default_value__"
 
-class TextParameterItem(
+internal class TextParameterItem(
     codebase: TextCodebase,
     private var name: String,
     private var publicName: String?,
@@ -76,7 +77,7 @@
 
     override fun toString(): String = "parameter ${name()}"
 
-    internal fun duplicate(typeVariableMap: Map<TypeItem, TypeItem>): TextParameterItem {
+    internal fun duplicate(typeVariableMap: TypeParameterBindings): TextParameterItem {
         return TextParameterItem(
             codebase,
             name,
diff --git a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextPropertyItem.kt b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextPropertyItem.kt
index 5836b5f..c5dfc23 100644
--- a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextPropertyItem.kt
+++ b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextPropertyItem.kt
@@ -21,7 +21,7 @@
 import com.android.tools.metalava.model.PropertyItem
 import com.android.tools.metalava.model.TypeItem
 
-class TextPropertyItem(
+internal class TextPropertyItem(
     codebase: TextCodebase,
     name: String,
     containingClass: TextClassItem,
diff --git a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextTypeItem.kt b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextTypeItem.kt
index 1c1ad46..dc87b97 100644
--- a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextTypeItem.kt
+++ b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextTypeItem.kt
@@ -17,87 +17,46 @@
 package com.android.tools.metalava.model.text
 
 import com.android.tools.metalava.model.ArrayTypeItem
-import com.android.tools.metalava.model.ClassItem
 import com.android.tools.metalava.model.ClassTypeItem
 import com.android.tools.metalava.model.DefaultTypeItem
 import com.android.tools.metalava.model.PrimitiveTypeItem
+import com.android.tools.metalava.model.ReferenceTypeItem
+import com.android.tools.metalava.model.TypeArgumentTypeItem
 import com.android.tools.metalava.model.TypeItem
 import com.android.tools.metalava.model.TypeNullability
 import com.android.tools.metalava.model.TypeParameterItem
 import com.android.tools.metalava.model.VariableTypeItem
 import com.android.tools.metalava.model.WildcardTypeItem
 
-sealed class TextTypeItem(open val codebase: TextCodebase) : DefaultTypeItem(codebase) {
-
-    override fun asClass(): ClassItem? {
-        if (this is PrimitiveTypeItem) {
-            return null
-        }
-        val cls = run {
-            val erased = toErasedTypeString()
-            // Also chop off array dimensions
-            val index = erased.indexOf('[')
-            if (index != -1) {
-                erased.substring(0, index)
-            } else {
-                erased
-            }
-        }
-        return codebase.getOrCreateClass(cls)
-    }
+internal sealed class TextTypeItem(
+    val codebase: TextCodebase,
+    override val modifiers: TextTypeModifiers,
+) : DefaultTypeItem(codebase) {
 
     internal abstract fun duplicate(withNullability: TypeNullability): TextTypeItem
-
-    companion object {
-
-        fun eraseTypeArguments(s: String): String {
-            val index = s.indexOf('<')
-            if (index != -1) {
-                var balance = 0
-                for (i in index..s.length) {
-                    val c = s[i]
-                    if (c == '<') {
-                        balance++
-                    } else if (c == '>') {
-                        balance--
-                        if (balance == 0) {
-                            return if (i == s.length - 1) {
-                                s.substring(0, index)
-                            } else {
-                                s.substring(0, index) + s.substring(i + 1)
-                            }
-                        }
-                    }
-                }
-
-                return s.substring(0, index)
-            }
-            return s
-        }
-    }
 }
 
 /** A [PrimitiveTypeItem] parsed from a signature file. */
 internal class TextPrimitiveTypeItem(
-    override val codebase: TextCodebase,
+    codebase: TextCodebase,
     override val kind: PrimitiveTypeItem.Primitive,
-    override val modifiers: TextTypeModifiers
-) : PrimitiveTypeItem, TextTypeItem(codebase) {
+    modifiers: TextTypeModifiers
+) : PrimitiveTypeItem, TextTypeItem(codebase, modifiers) {
     override fun duplicate(withNullability: TypeNullability): TextTypeItem {
         return TextPrimitiveTypeItem(codebase, kind, modifiers.duplicate(withNullability))
     }
 
     // Text types are immutable, so the modifiers don't actually need to be duplicated.
-    override fun duplicate(): TypeItem = this
+    override fun duplicate(): PrimitiveTypeItem = this
 }
 
 /** An [ArrayTypeItem] parsed from a signature file. */
 internal class TextArrayTypeItem(
-    override val codebase: TextCodebase,
+    codebase: TextCodebase,
     override val componentType: TypeItem,
     override val isVarargs: Boolean,
-    override val modifiers: TextTypeModifiers
-) : ArrayTypeItem, TextTypeItem(codebase) {
+    modifiers: TextTypeModifiers
+) : ArrayTypeItem, TextTypeItem(codebase, modifiers) {
     override fun duplicate(withNullability: TypeNullability): TextTypeItem {
         return TextArrayTypeItem(
             codebase,
@@ -114,36 +73,45 @@
 
 /** A [ClassTypeItem] parsed from a signature file. */
 internal class TextClassTypeItem(
-    override val codebase: TextCodebase,
+    codebase: TextCodebase,
     override val qualifiedName: String,
-    override val parameters: List<TypeItem>,
+    override val arguments: List<TypeArgumentTypeItem>,
     override val outerClassType: ClassTypeItem?,
-    override val modifiers: TextTypeModifiers
-) : ClassTypeItem, TextTypeItem(codebase) {
+    modifiers: TextTypeModifiers
+) : ClassTypeItem, TextTypeItem(codebase, modifiers) {
     override val className: String = ClassTypeItem.computeClassName(qualifiedName)
 
+    private val asClassCache by
+        lazy(LazyThreadSafetyMode.NONE) { codebase.resolveClass(qualifiedName) }
+
+    override fun asClass() = asClassCache
+
     override fun duplicate(withNullability: TypeNullability): TextTypeItem {
         return TextClassTypeItem(
             codebase,
             qualifiedName,
-            parameters,
+            arguments,
             outerClassType,
             modifiers.duplicate(withNullability)
         )
     }
 
-    override fun duplicate(outerClass: ClassTypeItem?, parameters: List<TypeItem>): ClassTypeItem {
-        return TextClassTypeItem(codebase, qualifiedName, parameters, outerClass, modifiers)
+    override fun duplicate(
+        outerClass: ClassTypeItem?,
+        arguments: List<TypeArgumentTypeItem>
+    ): ClassTypeItem {
+        return TextClassTypeItem(codebase, qualifiedName, arguments, outerClass, modifiers)
     }
 }
 
 /** A [VariableTypeItem] parsed from a signature file. */
 internal class TextVariableTypeItem(
-    override val codebase: TextCodebase,
+    codebase: TextCodebase,
     override val name: String,
     override val asTypeParameter: TypeParameterItem,
-    override val modifiers: TextTypeModifiers
-) : VariableTypeItem, TextTypeItem(codebase) {
+    modifiers: TextTypeModifiers
+) : VariableTypeItem, TextTypeItem(codebase, modifiers) {
+
     override fun duplicate(withNullability: TypeNullability): TextTypeItem {
         return TextVariableTypeItem(
             codebase,
@@ -154,16 +122,16 @@
     }
 
     // Text types are immutable, so the modifiers don't actually need to be duplicated.
-    override fun duplicate(): TypeItem = this
+    override fun duplicate(): VariableTypeItem = this
 }
 
 /** A [WildcardTypeItem] parsed from a signature file. */
 internal class TextWildcardTypeItem(
-    override val codebase: TextCodebase,
-    override val extendsBound: TypeItem?,
-    override val superBound: TypeItem?,
-    override val modifiers: TextTypeModifiers
-) : WildcardTypeItem, TextTypeItem(codebase) {
+    codebase: TextCodebase,
+    override val extendsBound: ReferenceTypeItem?,
+    override val superBound: ReferenceTypeItem?,
+    modifiers: TextTypeModifiers
+) : WildcardTypeItem, TextTypeItem(codebase, modifiers) {
     override fun duplicate(withNullability: TypeNullability): TextTypeItem {
         return TextWildcardTypeItem(
             codebase,
@@ -173,7 +141,10 @@
         )
     }
 
-    override fun duplicate(extendsBound: TypeItem?, superBound: TypeItem?): WildcardTypeItem {
+    override fun duplicate(
+        extendsBound: ReferenceTypeItem?,
+        superBound: ReferenceTypeItem?
+    ): WildcardTypeItem {
         return TextWildcardTypeItem(codebase, extendsBound, superBound, modifiers)
     }
 }
diff --git a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextTypeParameterItem.kt b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextTypeParameterItem.kt
index dada840..6bfde34 100644
--- a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextTypeParameterItem.kt
+++ b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextTypeParameterItem.kt
@@ -16,30 +16,41 @@
 
 package com.android.tools.metalava.model.text
 
+import com.android.tools.metalava.model.BoundsTypeItem
 import com.android.tools.metalava.model.DefaultModifierList
-import com.android.tools.metalava.model.TypeItem
 import com.android.tools.metalava.model.TypeParameterItem
-import com.android.tools.metalava.model.TypeParameterList
-import com.android.tools.metalava.model.TypeParameterListOwner
 
-class TextTypeParameterItem(
+internal class TextTypeParameterItem(
     codebase: TextCodebase,
-    private var owner: TypeParameterListOwner?,
-    private val typeParameterString: String,
-    name: String,
+    private val name: String,
     private val isReified: Boolean,
-    private var bounds: List<TypeItem>? = null,
 ) :
-    TextClassItem(
+    TextItem(
         codebase = codebase,
+        position = SourcePositionInfo.UNKNOWN,
         modifiers = DefaultModifierList(codebase, DefaultModifierList.PUBLIC),
-        name = name,
-        qualifiedName = name,
-        typeParameterList = TypeParameterList.NONE
     ),
     TypeParameterItem {
 
-    override fun toType(): TextTypeItem {
+    lateinit var bounds: List<BoundsTypeItem>
+
+    override fun name(): String {
+        return name
+    }
+
+    override fun toString() =
+        if (bounds.isEmpty() && !isReified) name
+        else
+            buildString {
+                if (isReified) append("reified ")
+                append(name)
+                if (bounds.isNotEmpty()) {
+                    append(" extends ")
+                    bounds.joinTo(this, " & ")
+                }
+            }
+
+    override fun type(): TextVariableTypeItem {
         return TextVariableTypeItem(
             codebase,
             name,
@@ -48,33 +59,35 @@
         )
     }
 
-    override fun typeBounds(): List<TypeItem> {
-        if (bounds == null) {
-            val boundsStringList = bounds(typeParameterString, owner)
-            bounds =
-                if (boundsStringList.isEmpty()) {
-                    emptyList()
-                } else {
-                    boundsStringList.map {
-                        codebase.typeResolver.obtainTypeFromString(it, gatherTypeParams(owner))
-                    }
-                }
-        }
-        return bounds!!
-    }
+    override fun typeBounds(): List<BoundsTypeItem> = bounds
 
     override fun isReified(): Boolean = isReified
 
-    internal fun setOwner(newOwner: TypeParameterListOwner) {
-        owner = newOwner
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is TypeParameterItem) return false
+
+        return name == other.name()
+    }
+
+    override fun hashCode(): Int {
+        return name.hashCode()
     }
 
     companion object {
+
+        /**
+         * Create a partially initialized [TextTypeParameterItem].
+         *
+         * This extracts the [isReified] and [name] from the [typeParameterString] and creates a
+         * [TextTypeParameterItem] with those properties initialized but the [bounds] is not.
+         *
+         * This must ONLY be used by [TextTypeParameterList.create] as that will complete the
+         * initialization of the [bounds] property.
+         */
         fun create(
             codebase: TextCodebase,
-            owner: TypeParameterListOwner?,
             typeParameterString: String,
-            bounds: List<TypeItem>? = null
         ): TextTypeParameterItem {
             val length = typeParameterString.length
             var nameEnd = length
@@ -95,88 +108,12 @@
                 }
             }
             val name = typeParameterString.substring(nameStart, nameEnd)
+
             return TextTypeParameterItem(
                 codebase = codebase,
-                owner = owner,
-                typeParameterString = typeParameterString,
                 name = name,
                 isReified = isReified,
-                bounds = bounds
             )
         }
-
-        fun bounds(typeString: String?, owner: TypeParameterListOwner? = null): List<String> {
-            val s = typeString ?: return emptyList()
-            val index = s.indexOf("extends ")
-            if (index == -1) {
-                // See if this is a type variable that has bounds in the parent
-                val parameters =
-                    (owner as? TextMemberItem)
-                        ?.containingClass()
-                        ?.typeParameterList()
-                        ?.typeParameters()
-                        ?: return emptyList()
-                for (p in parameters) {
-                    if (p.simpleName() == s) {
-                        return p.typeBounds().map { it.toTypeString() }
-                    }
-                }
-
-                return emptyList()
-            }
-            val list = mutableListOf<String>()
-            var angleBracketBalance = 0
-            var start = index + "extends ".length
-            val length = s.length
-            for (i in start until length) {
-                val c = s[i]
-                if (c == '&' && angleBracketBalance == 0) {
-                    add(list, typeString, start, i)
-                    start = i + 1
-                } else if (c == '<') {
-                    angleBracketBalance++
-                } else if (c == '>') {
-                    angleBracketBalance--
-                    if (angleBracketBalance == 0) {
-                        add(list, typeString, start, i + 1)
-                        start = i + 1
-                    }
-                }
-            }
-            if (start < length) {
-                add(list, typeString, start, length)
-            }
-            return list
-        }
-
-        private fun add(list: MutableList<String>, s: String, from: Int, to: Int) {
-            for (i in from until to) {
-                if (!Character.isWhitespace(s[i])) {
-                    var end = to
-                    while (end > i && s[end - 1].isWhitespace()) {
-                        end--
-                    }
-                    var begin = i
-                    while (begin < end && s[begin].isWhitespace()) {
-                        begin++
-                    }
-                    if (begin == end) {
-                        return
-                    }
-                    val element = s.substring(begin, end)
-                    list.add(element)
-                    return
-                }
-            }
-        }
-
-        /** Collect all the type parameters in scope for the given [owner]. */
-        private fun gatherTypeParams(owner: TypeParameterListOwner?): List<TypeParameterItem> {
-            return owner?.let {
-                it.typeParameterList().typeParameters() +
-                    gatherTypeParams(owner.typeParameterListOwnerParent())
-            }
-                ?: emptyList()
-        }
     }
 }
diff --git a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextTypeParameterList.kt b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextTypeParameterList.kt
index 84d1073..52da2f0 100644
--- a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextTypeParameterList.kt
+++ b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextTypeParameterList.kt
@@ -18,42 +18,24 @@
 
 import com.android.tools.metalava.model.TypeParameterItem
 import com.android.tools.metalava.model.TypeParameterList
-import com.android.tools.metalava.model.TypeParameterListOwner
 
-class TextTypeParameterList(
+internal class TextTypeParameterList(
     val codebase: TextCodebase,
-    private var owner: TypeParameterListOwner?,
-    private val typeListString: String
+    private val typeParameters: List<TextTypeParameterItem>,
 ) : TypeParameterList {
-    private var typeParameters: List<TextTypeParameterItem>? = null
-
-    override fun toString(): String = typeListString
+    override fun toString() = typeParameters.joinToString(prefix = "<", postfix = ">")
 
     override fun typeParameters(): List<TypeParameterItem> {
-        if (typeParameters == null) {
-            val strings = TextTypeParser.typeParameterStrings(typeListString)
-            val list = ArrayList<TextTypeParameterItem>(strings.size)
-            strings.mapTo(list) { TextTypeParameterItem.create(codebase, owner, it) }
-            typeParameters = list
-        }
-        return typeParameters!!
-    }
-
-    internal fun setOwner(newOwner: TypeParameterListOwner) {
-        owner = newOwner
-        typeParameters?.forEach { it.setOwner(newOwner) }
+        return typeParameters
     }
 
     companion object {
-        /**
-         * Creates a [TextTypeParameterList] without a set owner, for type parameters created before
-         * their owners are. The owner should be set after it is created.
-         *
-         * The [typeListString] should be the string representation of a list of type parameters,
-         * like "<A>" or "<A, B extends java.lang.String, C>".
-         */
-        fun create(codebase: TextCodebase, typeListString: String): TypeParameterList {
-            return TextTypeParameterList(codebase, owner = null, typeListString)
+        /** Creates a [TextTypeParameterList]. */
+        fun create(
+            codebase: TextCodebase,
+            typeParameters: List<TextTypeParameterItem>,
+        ): TypeParameterList {
+            return TextTypeParameterList(codebase, typeParameters)
         }
     }
 }
diff --git a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextTypeParser.kt b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextTypeParser.kt
index 4fbd96d..4633e93 100644
--- a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextTypeParser.kt
+++ b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TextTypeParser.kt
@@ -16,59 +16,119 @@
 
 package com.android.tools.metalava.model.text
 
+import com.android.tools.metalava.model.ClassTypeItem
+import com.android.tools.metalava.model.JAVA_LANG_ANNOTATION
 import com.android.tools.metalava.model.JAVA_LANG_OBJECT
 import com.android.tools.metalava.model.PrimitiveTypeItem
+import com.android.tools.metalava.model.ReferenceTypeItem
+import com.android.tools.metalava.model.TypeArgumentTypeItem
 import com.android.tools.metalava.model.TypeNullability
-import com.android.tools.metalava.model.TypeParameterItem
-import java.util.HashMap
+import com.android.tools.metalava.model.TypeUse
+import kotlin.collections.HashMap
 
 /** Parses and caches types for a [codebase]. */
-internal class TextTypeParser(val codebase: TextCodebase, var kotlinStyleNulls: Boolean = false) {
-    private val typeCache = Cache<String, TextTypeItem>()
+internal class TextTypeParser(val codebase: TextCodebase, val kotlinStyleNulls: Boolean = false) {
 
     /**
-     * Creates a [TextTypeItem] representing the type of [cl]. Since this is definitely a class
-     * type, the steps in [obtainTypeFromString] aren't needed.
+     * The cache key, incorporates the [TypeUse] as well as the type string as the [TypeUse] can
+     * affect the created [TypeItem].
+     *
+     * e.g. [TypeUse.SUPER_TYPE] will cause the type to be treated as a super class and so always be
+     * [TypeNullability.NONNULL] even if [kotlinStyleNulls] is `false` which would normally cause it
+     * to be [TypeNullability.PLATFORM].
      */
-    fun obtainTypeFromClass(cl: TextClassItem): TextTypeItem {
-        val params = cl.typeParameterList.typeParameters().map { it.toType() }
-        return TextClassTypeItem(codebase, cl.qualifiedName, params, null, emptyModifiers)
+    private data class Key(val typeUse: TypeUse, val type: String)
+
+    /** The cache from [Key] to [TextTypeItem]. */
+    private val typeCache = HashMap<Key, TextTypeItem>()
+
+    internal var requests = 0
+    internal var cacheSkip = 0
+    internal var cacheHit = 0
+    internal val cacheSize
+        get() = typeCache.size
+
+    /** [TextTypeModifiers] that are empty but set [TextTypeModifiers.nullability] to null. */
+    private val nonNullTypeModifiers =
+        TextTypeModifiers.create(codebase, emptyList(), TypeNullability.NONNULL)
+
+    /** A [JAVA_LANG_ANNOTATION] suitable for use as a super type. */
+    val superAnnotationType
+        get() = createJavaLangSuperType(JAVA_LANG_ANNOTATION)
+
+    /**
+     * Create a [ClassTypeItem] for a standard java.lang class suitable for use by a super class or
+     * interface.
+     */
+    private fun createJavaLangSuperType(standardClassName: String): ClassTypeItem {
+        return getSuperType(standardClassName, TypeParameterScope.empty)
     }
 
-    /** Creates or retrieves from cache a [TextTypeItem] representing `java.lang.Object` */
-    fun obtainObjectType(): TextTypeItem {
-        return typeCache.obtain(JAVA_LANG_OBJECT) {
-            TextClassTypeItem(codebase, JAVA_LANG_OBJECT, emptyList(), null, emptyModifiers)
-        }
-    }
+    /** A [TextTypeItem] representing `java.lang.Object`, suitable for general use. */
+    private val objectType: ReferenceTypeItem
+        get() = cachedParseType(JAVA_LANG_OBJECT, TypeParameterScope.empty) as ReferenceTypeItem
+
+    /**
+     * Creates or retrieves a previously cached [ClassTypeItem] that is suitable for use as a super
+     * type, e.g. in an `extends` or `implements` list.
+     */
+    fun getSuperType(
+        type: String,
+        typeParameterScope: TypeParameterScope,
+    ): ClassTypeItem =
+        obtainTypeFromString(type, typeParameterScope, TypeUse.SUPER_TYPE) as ClassTypeItem
 
     /**
      * Creates or retrieves from the cache a [TextTypeItem] representing [type], in the context of
-     * the type parameters from [typeParams], if applicable.
-     *
-     * The [annotations] are optional leading type-use annotations that have already been removed
-     * from the type string.
+     * the type parameters from [typeParameterScope], if applicable.
      */
     fun obtainTypeFromString(
         type: String,
-        typeParams: List<TypeParameterItem> = emptyList(),
-        annotations: List<String> = emptyList()
+        typeParameterScope: TypeParameterScope,
+        typeUse: TypeUse = TypeUse.GENERAL,
+    ): TextTypeItem = cachedParseType(type, typeParameterScope, emptyList(), typeUse)
+
+    /**
+     * Creates or retrieves from the cache a [TextTypeItem] representing [type], in the context of
+     * the type parameters from [typeParameterScope], if applicable.
+     *
+     * Used internally, as it has an extra [annotations] parameter that allows the annotations on
+     * array components to be correctly associated with the correct component. They are optional
+     * leading type-use annotations that have already been removed from the arrays type string.
+     */
+    private fun cachedParseType(
+        type: String,
+        typeParameterScope: TypeParameterScope,
+        annotations: List<String> = emptyList(),
+        typeUse: TypeUse = TypeUse.GENERAL,
     ): TextTypeItem {
+        requests++
         // Only use the cache if there are no type parameters to prevent identically named type
         // variables from different contexts being parsed as the same type.
         // Also don't use the cache when there are type-use annotations not contained in the string.
-        return if (typeParams.isEmpty() && annotations.isEmpty()) {
-            typeCache.obtain(type) { parseType(it, typeParams, annotations) }
+        return if (typeParameterScope.isEmpty() && annotations.isEmpty()) {
+            val key = Key(typeUse, type)
+
+            // Check it in the cache and if not found then create it and put it into the cache
+            typeCache[key]?.also { cacheHit++ }
+                ?: run {
+                    // Create it, cache it and return
+                    parseType(type, typeParameterScope, annotations, typeUse).also {
+                        typeCache[key] = it
+                    }
+                }
         } else {
-            parseType(type, typeParams, annotations)
+            cacheSkip++
+            parseType(type, typeParameterScope, annotations, typeUse)
         }
     }
 
-    /** Converts the [type] to a [TextTypeItem] in the context of the [typeParams]. */
+    /** Converts the [type] to a [TextTypeItem] in the context of the [typeParameterScope]. */
     private fun parseType(
         type: String,
-        typeParams: List<TypeParameterItem>,
-        annotations: List<String> = emptyList()
+        typeParameterScope: TypeParameterScope,
+        annotations: List<String>,
+        typeUse: TypeUse = TypeUse.GENERAL,
     ): TextTypeItem {
         val (unannotated, annotationsFromString) = trimLeadingAnnotations(type)
         val allAnnotations = annotations + annotationsFromString
@@ -78,15 +138,15 @@
 
         // Figure out what kind of type this is. Start with the simple cases: primitive or variable.
         return asPrimitive(type, trimmed, allAnnotations, nullability)
-            ?: asVariable(trimmed, typeParams, allAnnotations, nullability)
+            ?: asVariable(trimmed, typeParameterScope, allAnnotations, nullability)
             // Try parsing as a wildcard before trying to parse as an array.
             // `? extends java.lang.String[]` should be parsed as a wildcard with an array bound,
             // not as an array of wildcards, for consistency with how this would be compiled.
-            ?: asWildcard(trimmed, typeParams, allAnnotations, nullability)
+            ?: asWildcard(trimmed, typeParameterScope, allAnnotations, nullability)
             // Try parsing as an array.
-            ?: asArray(trimmed, allAnnotations, nullability, typeParams)
+            ?: asArray(trimmed, allAnnotations, nullability, typeParameterScope)
             // If it isn't anything else, parse the type as a class.
-            ?: asClass(trimmed, typeParams, allAnnotations, nullability)
+            ?: asClass(trimmed, typeUse, typeParameterScope, allAnnotations, nullability)
     }
 
     /**
@@ -130,13 +190,13 @@
      * Try parsing [type] as an array. This will return a non-null [TextArrayTypeItem] if [type]
      * ends with `[]` or `...`.
      *
-     * The context [typeParams] are used to parse the component type of the array.
+     * The context [typeParameterScope] are used to parse the component type of the array.
      */
     private fun asArray(
         type: String,
         componentAnnotations: List<String>,
         nullability: TypeNullability?,
-        typeParams: List<TypeParameterItem>
+        typeParameterScope: TypeParameterScope
     ): TextArrayTypeItem? {
         // Check if this is a regular array or varargs.
         val (inner, varargs) =
@@ -194,7 +254,7 @@
         // the leading annotations already removed from the type string.
         componentString += componentNullability?.suffix.orEmpty()
         val deepComponentType =
-            obtainTypeFromString(componentString, typeParams, componentAnnotations)
+            cachedParseType(componentString, typeParameterScope, componentAnnotations)
 
         // Join the annotations and nullability markers -- as described in the comment above, these
         // appear in the string in reverse order of each other. The modifiers list will be ordered
@@ -220,13 +280,13 @@
      * Try parsing [type] as a wildcard. This will return a non-null [TextWildcardTypeItem] if
      * [type] begins with `?`.
      *
-     * The context [typeParams] are needed to parse the bounds of the wildcard.
+     * The context [typeParameterScope] are needed to parse the bounds of the wildcard.
      *
      * [type] should have annotations and nullability markers stripped.
      */
     private fun asWildcard(
         type: String,
-        typeParams: List<TypeParameterItem>,
+        typeParameterScope: TypeParameterScope,
         annotations: List<String>,
         nullability: TypeNullability?
     ): TextWildcardTypeItem? {
@@ -237,7 +297,7 @@
         if (type == "?")
             return TextWildcardTypeItem(
                 codebase,
-                obtainObjectType(),
+                objectType,
                 null,
                 modifiers(annotations, TypeNullability.UNDEFINED)
             )
@@ -248,7 +308,7 @@
             val extendsBound = bound.substring(8)
             TextWildcardTypeItem(
                 codebase,
-                obtainTypeFromString(extendsBound, typeParams),
+                getWildcardBound(extendsBound, typeParameterScope),
                 null,
                 modifiers(annotations, TypeNullability.UNDEFINED)
             )
@@ -257,8 +317,8 @@
             TextWildcardTypeItem(
                 codebase,
                 // All wildcards have an implicit Object extends bound
-                obtainObjectType(),
-                obtainTypeFromString(superBound, typeParams),
+                objectType,
+                getWildcardBound(superBound, typeParameterScope),
                 modifiers(annotations, TypeNullability.UNDEFINED)
             )
         } else {
@@ -268,19 +328,22 @@
         }
     }
 
+    private fun getWildcardBound(bound: String, typeParameterScope: TypeParameterScope) =
+        cachedParseType(bound, typeParameterScope) as ReferenceTypeItem
+
     /**
      * Try parsing [type] as a type variable. This will return a non-null [TextVariableTypeItem] if
-     * [type] matches a parameter from [typeParams].
+     * [type] matches a parameter from [typeParameterScope].
      *
      * [type] should have annotations and nullability markers stripped.
      */
     private fun asVariable(
         type: String,
-        typeParams: List<TypeParameterItem>,
+        typeParameterScope: TypeParameterScope,
         annotations: List<String>,
         nullability: TypeNullability?
     ): TextVariableTypeItem? {
-        val param = typeParams.firstOrNull { it.simpleName() == type } ?: return null
+        val param = typeParameterScope.findTypeParameter(type) ?: return null
         return TextVariableTypeItem(codebase, type, param, modifiers(annotations, nullability))
     }
 
@@ -288,17 +351,18 @@
      * Parse the [type] as a class. This function will always return a non-null [TextClassTypeItem],
      * so it should only be used when it is certain that [type] is not a different kind of type.
      *
-     * The context [typeParams] are used to parse the parameters of the class type.
+     * The context [typeParameterScope] are used to parse the parameters of the class type.
      *
      * [type] should have annotations and nullability markers stripped.
      */
     private fun asClass(
         type: String,
-        typeParams: List<TypeParameterItem>,
+        typeUse: TypeUse,
+        typeParameterScope: TypeParameterScope,
         annotations: List<String>,
         nullability: TypeNullability?
     ): TextClassTypeItem {
-        return createClassType(type, null, typeParams, annotations, nullability)
+        return createClassType(type, typeUse, null, typeParameterScope, annotations, nullability)
     }
 
     /**
@@ -309,8 +373,9 @@
      */
     private fun createClassType(
         type: String,
+        typeUse: TypeUse,
         outerClassType: TextClassTypeItem?,
-        typeParams: List<TypeParameterItem>,
+        typeParameterScope: TypeParameterScope,
         annotations: List<String>,
         nullability: TypeNullability?
     ): TextClassTypeItem {
@@ -327,18 +392,21 @@
                 name
             }
 
-        val (paramStrings, remainder) = typeParameterStringsWithRemainder(afterName)
-        val params = paramStrings.map { obtainTypeFromString(it, typeParams) }
+        val (argumentStrings, remainder) = typeParameterStringsWithRemainder(afterName)
+        val arguments =
+            argumentStrings.map { cachedParseType(it, typeParameterScope) as TypeArgumentTypeItem }
         // If this is an outer class type (there's a remainder), call it non-null and don't apply
         // the leading annotations (they belong to the inner class type).
         val classModifiers =
             if (remainder != null) {
                 modifiers(classAnnotations, TypeNullability.NONNULL)
             } else {
-                modifiers(classAnnotations + annotations, nullability)
+                val actualNullability =
+                    if (typeUse == TypeUse.SUPER_TYPE) TypeNullability.NONNULL else nullability
+                modifiers(classAnnotations + annotations, actualNullability)
             }
         val classType =
-            TextClassTypeItem(codebase, qualifiedName, params, outerClassType, classModifiers)
+            TextClassTypeItem(codebase, qualifiedName, arguments, outerClassType, classModifiers)
 
         if (remainder != null) {
             if (!remainder.startsWith('.')) {
@@ -349,8 +417,10 @@
             // This is an inner class type, recur with the new outer class
             return createClassType(
                 remainder.substring(1),
+                // An inner class has the same type use as the outer class.
+                typeUse,
                 classType,
-                typeParams,
+                typeParameterScope,
                 annotations,
                 nullability
             )
@@ -359,9 +429,6 @@
         return classType
     }
 
-    private val emptyModifiers: TextTypeModifiers =
-        TextTypeModifiers.create(codebase, emptyList(), null)
-
     private fun modifiers(
         annotations: List<String>,
         nullability: TypeNullability?
@@ -369,20 +436,6 @@
         return TextTypeModifiers.create(codebase, annotations, nullability)
     }
 
-    private class Cache<Key, Value> {
-        private val cache = HashMap<Key, Value>()
-
-        fun obtain(o: Key, make: (Key) -> Value): Value {
-            var r = cache[o]
-            if (r == null) {
-                r = make(o)
-                cache[o] = r
-            }
-            // r must be non-null: either it was cached or created with make
-            return r!!
-        }
-    }
-
     companion object {
         /**
          * Splits the Kotlin-style nullability marker off the type string, returning a pair of the
diff --git a/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TypeParameterScope.kt b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TypeParameterScope.kt
new file mode 100644
index 0000000..6f6a5c6
--- /dev/null
+++ b/metalava-model-text/src/main/java/com/android/tools/metalava/model/text/TypeParameterScope.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.model.text
+
+import com.android.tools.metalava.model.ClassItem
+import com.android.tools.metalava.model.TypeParameterItem
+
+/**
+ * The set of [TypeParameterItem]s that are in scope.
+ *
+ * This is used to resolve a reference to a type parameter to the appropriate type parameter. They
+ * are in order from closest to most distant. e.g. When resolving the type parameters for `method`
+ * this will contain the type parameters from `method` then from `Inner`, then from `Outer`, i.e.
+ * `[M, X, I, X, O, X]`. That ensures that when searching for a type parameter whose name shadows
+ * one from an outer scope, e.g. `X`, that the inner one is used.
+ *
+ * ```
+ *     public class Outer<O, X> {
+ *     }
+ *
+ *     public class Outer.Inner<I, X> {
+ *       method public <M, X> M method(O o, I i);
+ *     }
+ * ```
+ */
+internal sealed class TypeParameterScope private constructor() {
+
+    /** True if there are no type parameters in scope. */
+    fun isEmpty() = count == 0
+
+    /**
+     * Create a nested [TypeParameterScope] that will delegate to this one for any
+     * [TypeParameterItem]s that it cannot find.
+     */
+    fun nestedScope(typeParameters: List<TypeParameterItem>) =
+        // If the typeParameters is empty then just reuse this one, otherwise create a new scope
+        // delegating to this.
+        if (typeParameters.isEmpty()) this else ListWrapper(typeParameters, this)
+
+    /** Finds the closest [TypeParameterItem] with the specified name. */
+    abstract fun findTypeParameter(name: String): TypeParameterItem?
+
+    protected abstract val count: Int
+
+    companion object {
+        val empty: TypeParameterScope = Empty
+
+        /**
+         * Collect all the type parameters in scope for the given [owner] then wrap them in an
+         * [TypeParameterScope].
+         */
+        fun from(owner: ClassItem?): TypeParameterScope {
+            return if (owner == null) empty
+            else {
+                // Construct a scope from the owner.
+                from(owner.containingClass())
+                    // Nest this inside it.
+                    .nestedScope(owner.typeParameterList().typeParameters())
+            }
+        }
+    }
+
+    private class ListWrapper(
+        private val list: List<TypeParameterItem>,
+        private val enclosingScope: TypeParameterScope
+    ) : TypeParameterScope() {
+
+        override val count: Int = list.size + enclosingScope.count
+
+        override fun findTypeParameter(name: String) =
+            // Search in this scope first, then delegate to the parent.
+            list.firstOrNull { it.name() == name } ?: enclosingScope.findTypeParameter(name)
+    }
+
+    private object Empty : TypeParameterScope() {
+
+        override val count: Int = 0
+
+        override fun findTypeParameter(name: String) = null
+    }
+}
diff --git a/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/ApiFileTest.kt b/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/ApiFileTest.kt
index 1ae23ed..0eb6309 100644
--- a/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/ApiFileTest.kt
+++ b/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/ApiFileTest.kt
@@ -16,14 +16,57 @@
 
 package com.android.tools.metalava.model.text
 
-import com.android.tools.metalava.model.Assertions
+import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.metalava.model.ClassItem
 import com.android.tools.metalava.model.ClassResolver
+import com.android.tools.metalava.model.Codebase
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertEquals
 import kotlin.test.assertNull
 import kotlin.test.assertSame
+import org.junit.Assert.assertThrows
 import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
 
-class ApiFileTest : Assertions {
+@RunWith(Parameterized::class)
+class ApiFileTest : BaseTextCodebaseTest() {
+
+    @Test
+    fun `Test mixture of kotlinStyleNulls settings`() {
+        val exception =
+            assertThrows(ApiParseException::class.java) {
+                runSignatureTest(
+                    signature(
+                        "file1.txt",
+                        """
+                            // Signature format: 5.0
+                            // - kotlin-style-nulls=yes
+                            package test.pkg {
+                                public class Foo {
+                                    method void foo(Object);
+                                }
+                            }
+                        """
+                    ),
+                    signature(
+                        "file2.txt",
+                        """
+                            // Signature format: 5.0
+                            // - kotlin-style-nulls=no
+                            package test.pkg {
+                                public class Bar {
+                                    method void bar(Object);
+                                }
+                            }
+                        """
+                    )
+                ) {}
+            }
+
+        assertThat(exception.message)
+            .contains("Cannot mix signature files with different settings of kotlinStyleNulls")
+    }
 
     @Test
     fun `Test parse from InputStream`() {
@@ -37,9 +80,8 @@
 
     @Test
     fun `Test known Throwable`() {
-        val codebase =
-            ApiFile.parseApi(
-                "api.txt",
+        runSignatureTest(
+            signature(
                 """
                     // Signature format: 2.0
                     package java.lang {
@@ -52,24 +94,29 @@
                         }
                     }
                 """
-                    .trimIndent()
-            )
+            ),
+        ) {
+            val throwable = codebase.assertClass("java.lang.Throwable")
 
-        val objectClass = codebase.assertClass("java.lang.Object")
-        val throwable = codebase.assertClass("java.lang.Throwable")
-        assertSame(objectClass, throwable.superClass())
+            // Get the super class to force it to be loaded.
+            val throwableSuperClass = throwable.superClass()
 
-        // Make sure the stub Throwable is used in the throws types.
-        val exception =
-            codebase.assertClass("test.pkg.Foo").assertMethod("foo", "").throwsTypes().first()
-        assertSame(throwable, exception)
+            // Now get the object class.
+            val objectClass = codebase.assertClass("java.lang.Object")
+
+            assertSame(objectClass, throwableSuperClass)
+
+            // Make sure the stub Throwable is used in the throws types.
+            val exception =
+                codebase.assertClass("test.pkg.Foo").assertMethod("foo", "").throwsTypes().first()
+            assertSame(throwable, exception.classItem)
+        }
     }
 
     @Test
     fun `Test known Throwable subclass`() {
-        val codebase =
-            ApiFile.parseApi(
-                "api.txt",
+        runSignatureTest(
+            signature(
                 """
                     // Signature format: 2.0
                     package java.lang {
@@ -82,24 +129,29 @@
                         }
                     }
                 """
-                    .trimIndent()
-            )
+            ),
+        ) {
+            val error = codebase.assertClass("java.lang.Error")
 
-        val throwable = codebase.assertClass("java.lang.Throwable")
-        val error = codebase.assertClass("java.lang.Error")
-        assertSame(throwable, error.superClass())
+            // Get the super class to force it to be loaded.
+            val errorSuperClass = error.superClassType()?.asClass()
 
-        // Make sure the stub Throwable is used in the throws types.
-        val exception =
-            codebase.assertClass("test.pkg.Foo").assertMethod("foo", "").throwsTypes().first()
-        assertSame(error, exception)
+            // Now get the throwable class.
+            val throwable = codebase.assertClass("java.lang.Throwable")
+
+            assertSame(throwable, errorSuperClass)
+
+            // Make sure the stub Throwable is used in the throws types.
+            val exception =
+                codebase.assertClass("test.pkg.Foo").assertMethod("foo", "").throwsTypes().first()
+            assertSame(error, exception.classItem)
+        }
     }
 
     @Test
     fun `Test unknown Throwable`() {
-        val codebase =
-            ApiFile.parseApi(
-                "api.txt",
+        runSignatureTest(
+            signature(
                 """
                     // Signature format: 2.0
                     package test.pkg {
@@ -108,24 +160,23 @@
                         }
                     }
                 """
-                    .trimIndent()
-            )
+            ),
+        ) {
+            val throwable = codebase.assertClass("java.lang.Throwable")
+            // This should probably be Object.
+            assertNull(throwable.superClass())
 
-        val throwable = codebase.assertClass("java.lang.Throwable")
-        // This should probably be Object.
-        assertNull(throwable.superClass())
-
-        // Make sure the stub Throwable is used in the throws types.
-        val exception =
-            codebase.assertClass("test.pkg.Foo").assertMethod("foo", "").throwsTypes().first()
-        assertSame(throwable, exception)
+            // Make sure the stub Throwable is used in the throws types.
+            val exception =
+                codebase.assertClass("test.pkg.Foo").assertMethod("foo", "").throwsTypes().first()
+            assertSame(throwable, exception.classItem)
+        }
     }
 
     @Test
     fun `Test unknown Throwable subclass`() {
-        val codebase =
-            ApiFile.parseApi(
-                "api.txt",
+        runSignatureTest(
+            signature(
                 """
                     // Signature format: 2.0
                     package test.pkg {
@@ -134,18 +185,18 @@
                         }
                     }
                 """
-                    .trimIndent()
-            )
+            ),
+        ) {
+            val throwable = codebase.assertClass("java.lang.Throwable")
+            val unknownExceptionClass = codebase.assertClass("other.UnknownException")
+            // Make sure the stub UnknownException is initialized correctly.
+            assertSame(throwable, unknownExceptionClass.superClass())
 
-        val throwable = codebase.assertClass("java.lang.Throwable")
-        val unknownExceptionClass = codebase.assertClass("other.UnknownException")
-        // Make sure the stub UnknownException is initialized correctly.
-        assertSame(throwable, unknownExceptionClass.superClass())
-
-        // Make sure the stub UnknownException is used in the throws types.
-        val exception =
-            codebase.assertClass("test.pkg.Foo").assertMethod("foo", "").throwsTypes().first()
-        assertSame(unknownExceptionClass, exception)
+            // Make sure the stub UnknownException is used in the throws types.
+            val exception =
+                codebase.assertClass("test.pkg.Foo").assertMethod("foo", "").throwsTypes().first()
+            assertSame(unknownExceptionClass, exception.classItem)
+        }
     }
 
     @Test
@@ -176,13 +227,227 @@
         // types.
         val exception =
             codebase.assertClass("test.pkg.Foo").assertMethod("foo", "").throwsTypes().first()
-        assertSame(unknownExceptionClass, exception)
+        assertSame(unknownExceptionClass, exception.classItem)
+    }
+
+    @Test
+    fun `Test matching package annotations are allowed`() {
+        runSignatureTest(
+            signature(
+                "file1.txt",
+                """
+                    // Signature format: 2.0
+                    package @PackageAnnotation test.pkg {
+                        public class Foo {
+                        }
+                    }
+                """
+            ),
+            signature(
+                "file2.txt",
+                """
+                    // Signature format: 2.0
+                    package @PackageAnnotation test.pkg {
+                        public class Foo {
+                        }
+                    }
+                """
+            ),
+        ) {}
+    }
+
+    @Test
+    fun `Test different package annotations are not allowed`() {
+        val exception =
+            assertThrows(ApiParseException::class.java) {
+                runSignatureTest(
+                    signature(
+                        "file1.txt",
+                        """
+                            // Signature format: 2.0
+                            package @PackageAnnotation1 test.pkg {
+                                public class Foo {
+                                }
+                            }
+                        """
+                    ),
+                    signature(
+                        "file2.txt",
+                        """
+                            // Signature format: 2.0
+                            package @PackageAnnotation2 test.pkg {
+                                public class Foo {
+                                }
+                            }
+                        """
+                    ),
+                ) {}
+            }
+        assertThat(exception.message).contains("Contradicting declaration of package test.pkg")
+    }
+
+    /** Dump the package structure of [codebase] to a string for easy comparison. */
+    private fun dumpPackageStructure(codebase: Codebase) = buildString {
+        codebase.getPackages().packages.map { packageItem ->
+            append("${packageItem.qualifiedName()}\n")
+            for (classItem in packageItem.allClasses()) {
+                append("    ${classItem.qualifiedName()}\n")
+            }
+        }
+    }
+
+    /** Check that the package structure created from the [sources] matches what is expected. */
+    private fun checkPackageStructureCreatedCorrectly(vararg sources: TestFile) {
+        runSignatureTest(*sources) {
+            val data = dumpPackageStructure(codebase)
+
+            assertEquals(
+                """
+                    test.pkg
+                        test.pkg.Outer
+                        test.pkg.Outer.Middle
+                        test.pkg.Outer.Middle.Inner
+                """
+                    .trimIndent(),
+                data.trimEnd()
+            )
+        }
+    }
+
+    @Test
+    fun `Test missing all containing classes`() {
+        checkPackageStructureCreatedCorrectly(
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                        public class Outer.Middle.Inner {
+                        }
+                    }
+                """
+            ),
+        )
+    }
+
+    @Test
+    fun `Test missing outer class`() {
+        checkPackageStructureCreatedCorrectly(
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                        public class Outer.Middle {
+                        }
+                        public class Outer.Middle.Inner {
+                        }
+                    }
+                """
+            ),
+        )
+    }
+
+    @Test
+    fun `Test missing middle class`() {
+        checkPackageStructureCreatedCorrectly(
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                        public class Outer {
+                        }
+                        public class Outer.Middle.Inner {
+                        }
+                    }
+                """
+            ),
+        )
+    }
+
+    @Test
+    fun `Test split across multiple files, middle missing`() {
+        checkPackageStructureCreatedCorrectly(
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                        public class Outer {
+                        }
+                    }
+                """
+            ),
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                        public class Outer.Middle.Inner {
+                        }
+                    }
+                """
+            ),
+        )
+    }
+
+    @Test
+    fun `Test split across multiple files`() {
+        checkPackageStructureCreatedCorrectly(
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                        public class Outer {
+                        }
+                    }
+                """
+            ),
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                        public class Outer.Middle {
+                        }
+                    }
+                """
+            ),
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                        public class Outer.Middle.Inner {
+                        }
+                    }
+                """
+            ),
+        )
+    }
+
+    @Test
+    fun testTypeParameterNames() {
+        assertThat(ApiFile.extractTypeParameterBoundsStringList(null).toString()).isEqualTo("[]")
+        assertThat(ApiFile.extractTypeParameterBoundsStringList("").toString()).isEqualTo("[]")
+        assertThat(ApiFile.extractTypeParameterBoundsStringList("X").toString()).isEqualTo("[]")
+        assertThat(ApiFile.extractTypeParameterBoundsStringList("DEF extends T").toString())
+            .isEqualTo("[T]")
+        assertThat(
+                ApiFile.extractTypeParameterBoundsStringList(
+                        "T extends java.lang.Comparable<? super T>"
+                    )
+                    .toString()
+            )
+            .isEqualTo("[java.lang.Comparable<? super T>]")
+        assertThat(
+                ApiFile.extractTypeParameterBoundsStringList(
+                        "T extends java.util.List<Number> & java.util.RandomAccess"
+                    )
+                    .toString()
+            )
+            .isEqualTo("[java.util.List<Number>, java.util.RandomAccess]")
     }
 
     class TestClassItem private constructor(delegate: ClassItem) : ClassItem by delegate {
         companion object {
             fun create(name: String): TestClassItem {
-                val codebase = ApiFile.parseApi("other.txt", "// Signature format: 2.0")
+                val codebase =
+                    ApiFile.parseApi("other.txt", "// Signature format: 2.0") as TextCodebase
                 val delegate = codebase.getOrCreateClass(name)
                 return TestClassItem(delegate)
             }
diff --git a/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/BaseTextCodebaseTest.kt b/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/BaseTextCodebaseTest.kt
new file mode 100644
index 0000000..0406912
--- /dev/null
+++ b/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/BaseTextCodebaseTest.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.model.text
+
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.metalava.model.Codebase
+import com.android.tools.metalava.model.testsuite.BaseModelTest
+import com.android.tools.metalava.model.testsuite.InputFormat
+import com.android.tools.metalava.model.testsuite.TestParameters
+
+/**
+ * Base class for text test classes that parse signature files to create a [TextCodebase] that can
+ * then be introspected.
+ */
+open class BaseTextCodebaseTest :
+    BaseModelTest(TestParameters(TextModelSuiteRunner(), InputFormat.SIGNATURE)) {
+
+    /** Run a single signature test with a set of signature files. */
+    fun runSignatureTest(vararg sources: TestFile, test: CodebaseContext<Codebase>.() -> Unit) {
+        runCodebaseTest(inputSet(*sources), test = test)
+    }
+}
diff --git a/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/ClassLoaderBasedClassResolverTest.kt b/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/ClassLoaderBasedClassResolverTest.kt
new file mode 100644
index 0000000..da405d7
--- /dev/null
+++ b/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/ClassLoaderBasedClassResolverTest.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.model.text
+
+import com.android.tools.metalava.testing.getAndroidJar
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import org.junit.Test
+
+class ClassLoaderBasedClassResolverTest {
+
+    private fun checkClassResolved(qualifiedName: String) {
+        val resolver = ClassLoaderBasedClassResolver(getAndroidJar())
+        val classItem = resolver.resolveClass(qualifiedName)
+        assertNotNull(classItem)
+        assertEquals(qualifiedName, classItem.qualifiedName())
+    }
+
+    @Test
+    fun `Test object`() {
+        checkClassResolved("java.lang.Object")
+    }
+
+    @Test
+    fun `Test inner class`() {
+        checkClassResolved("java.util.Map.Entry")
+    }
+}
diff --git a/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/MultipleFileTest.kt b/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/MultipleFileTest.kt
new file mode 100644
index 0000000..48ef87e
--- /dev/null
+++ b/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/MultipleFileTest.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.model.text
+
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertSame
+import org.junit.Assert.assertThrows
+import org.junit.Test
+
+/** Contains tests for when loading multiple files into a single [TextCodebase]. */
+class MultipleFileTest : BaseTextCodebaseTest() {
+
+    @Test
+    fun `Test parse multiple files correctly updates super class`() {
+        val testFiles =
+            listOf(
+                signature(
+                    "first.txt",
+                    """
+                        // Signature format: 2.0
+                        package test.pkg {
+                            public class Foo {
+                            }
+                        }
+                    """
+                ),
+                signature(
+                    "second.txt",
+                    """
+                        // Signature format: 2.0
+                        package test.pkg {
+                            public class Bar {
+                            }
+                            public class Foo extends test.pkg.Bar {
+                            }
+                        }
+                    """
+                ),
+                signature(
+                    "third.txt",
+                    """
+                        // Signature format: 2.0
+                        package test.pkg {
+                            public class Bar {
+                            }
+                            public class Baz {
+                            }
+                            public class Foo extends test.pkg.Baz {
+                            }
+                        }
+                    """
+                ),
+            )
+
+        fun checkSuperClass(files: List<TestFile>, order: String, expectedSuperClass: String) {
+            runSignatureTest(*files.toTypedArray()) {
+                val fooClass = codebase.assertClass("test.pkg.Foo")
+                assertSame(
+                    codebase.assertClass(expectedSuperClass),
+                    fooClass.superClass(),
+                    message = "incorrect super class from $order"
+                )
+            }
+        }
+
+        // Order matters, the last, non-null super class wins.
+        checkSuperClass(testFiles, "narrowest to widest", "test.pkg.Baz")
+        checkSuperClass(testFiles.reversed(), "widest to narrowest", "test.pkg.Bar")
+    }
+
+    @Test
+    fun `Test generic class split across multiple files detect type parameter inconsistencies`() {
+        val exception =
+            assertThrows(ApiParseException::class.java) {
+                runSignatureTest(
+                    signature(
+                        "file1.txt",
+                        """
+                        // Signature format: 2.0
+                        package test.pkg {
+                          public class Generic<T, S extends Comparable<S>> {
+                          }
+                        }
+                    """
+                    ),
+                    signature(
+                        "file2.txt",
+                        """
+                        // Signature format: 2.0
+                        package test.pkg {
+                          public class Generic<S extends Comparable<S>, T> {
+                          }
+                        }
+                    """
+                    ),
+                ) {}
+            }
+
+        assertThat(exception.message)
+            .matches(
+                """.*\Q/file2.txt:3: Inconsistent type parameter list for test.pkg.Generic, this has <S extends java.lang.Comparable<S>, T> but it was previously defined as <T, S extends java.lang.Comparable<S>>\E at .*/file1.txt:3"""
+            )
+    }
+}
diff --git a/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextMethodItemTest.kt b/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextMethodItemTest.kt
index 74bb000..1181cb2 100644
--- a/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextMethodItemTest.kt
+++ b/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextMethodItemTest.kt
@@ -16,45 +16,43 @@
 
 package com.android.tools.metalava.model.text
 
-import com.android.tools.metalava.model.Assertions
 import kotlin.test.assertEquals
 import kotlin.test.assertNotNull
 import org.junit.Test
 
-class TextMethodItemTest : Assertions {
+class TextMethodItemTest : BaseTextCodebaseTest() {
 
     @Test
     fun `text method item return type is non-null`() {
-        val codebase =
-            ApiFile.parseApi(
-                "test",
+        runSignatureTest(
+            signature(
                 """
-            // Signature format: 2.0
-            package test.pkg {
-              public class Foo {
-                ctor public Foo();
-                method public void bar();
-              }
-            }
-            """
-                    .trimIndent(),
+                    // Signature format: 2.0
+                    package test.pkg {
+                      public class Foo {
+                        ctor public Foo();
+                        method public void bar();
+                      }
+                    }
+                """
             )
+        ) {
+            val cls = codebase.assertClass("test.pkg.Foo")
+            val ctorItem = cls.assertMethod("Foo", "")
+            val methodItem = cls.assertMethod("bar", "")
 
-        val cls = codebase.assertClass("test.pkg.Foo")
-        val ctorItem = cls.assertMethod("Foo", "")
-        val methodItem = cls.assertMethod("bar", "")
-
-        assertNotNull(ctorItem.returnType())
-        assertEquals(
-            "test.pkg.Foo",
-            ctorItem.returnType().toString(),
-            "Return type of the constructor item must be the containing class."
-        )
-        assertNotNull(methodItem.returnType())
-        assertEquals(
-            "void",
-            methodItem.returnType().toString(),
-            "Return type of an method item should match the expected value."
-        )
+            assertNotNull(ctorItem.returnType())
+            assertEquals(
+                "test.pkg.Foo",
+                ctorItem.returnType().toString(),
+                "Return type of the constructor item must be the containing class."
+            )
+            assertNotNull(methodItem.returnType())
+            assertEquals(
+                "void",
+                methodItem.returnType().toString(),
+                "Return type of an method item should match the expected value."
+            )
+        }
     }
 }
diff --git a/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextModelSuiteRunner.kt b/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextModelSuiteRunner.kt
index a06f333..ba6dc06 100644
--- a/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextModelSuiteRunner.kt
+++ b/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextModelSuiteRunner.kt
@@ -17,10 +17,16 @@
 package com.android.tools.metalava.model.text
 
 import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.metalava.model.ClassItem
+import com.android.tools.metalava.model.ClassResolver
 import com.android.tools.metalava.model.Codebase
+import com.android.tools.metalava.model.DefaultModifierList
+import com.android.tools.metalava.model.noOpAnnotationManager
 import com.android.tools.metalava.model.testsuite.InputFormat
 import com.android.tools.metalava.model.testsuite.ModelSuiteRunner
+import com.android.tools.metalava.testing.getAndroidJar
 import java.io.File
+import java.net.URLClassLoader
 
 // @AutoService(ModelSuiteRunner::class)
 class TextModelSuiteRunner : ModelSuiteRunner {
@@ -33,9 +39,85 @@
         test: (Codebase) -> Unit,
     ) {
         val signatureFiles = input.map { it.createFile(tempDir) }
-        val codebase = ApiFile.parseApi(signatureFiles)
+
+        val resolver = ClassLoaderBasedClassResolver(getAndroidJar())
+
+        val codebase = ApiFile.parseApi(signatureFiles, classResolver = resolver)
         test(codebase)
     }
 
     override fun toString(): String = "text"
 }
+
+/**
+ * A [ClassResolver] that is backed by a [URLClassLoader].
+ *
+ * When [resolveClass] is called this will first look in [codebase] to see if the [ClassItem] has
+ * already been loaded, returning it if found. Otherwise, it will look in the [classLoader] to see
+ * if the class exists on the classpath. If it does then it will create a [TextClassItem] to
+ * represent it and add it to the [codebase]. Otherwise, it will return `null`.
+ *
+ * The created [TextClassItem] is not a complete representation of the class that was found in the
+ * [classLoader]. It is just a placeholder to indicate that it was found, although that may change
+ * in the future.
+ */
+internal class ClassLoaderBasedClassResolver(jar: File) : ClassResolver {
+
+    private val codebase by lazy {
+        TextCodebase(
+            location = jar,
+            annotationManager = noOpAnnotationManager,
+            classResolver = null,
+        )
+    }
+
+    private val classLoader by lazy { URLClassLoader(arrayOf(jar.toURI().toURL()), null) }
+
+    private fun findClassInClassLoader(qualifiedName: String): Class<*>? {
+        var binaryName = qualifiedName
+        do {
+            try {
+                return classLoader.loadClass(binaryName)
+            } catch (e: ClassNotFoundException) {
+                // If the class could not be found then maybe it was an inner class so replace the
+                // last '.' in the name with a $ and try again. If there is no '.' then return.
+                val lastDot = binaryName.lastIndexOf('.')
+                if (lastDot == -1) {
+                    return null
+                } else {
+                    val before = binaryName.substring(0, lastDot)
+                    val after = binaryName.substring(lastDot + 1)
+                    binaryName = "$before\$$after"
+                }
+            }
+        } while (true)
+    }
+
+    override fun resolveClass(erasedName: String): ClassItem? {
+        return codebase.findClass(erasedName)
+            ?: run {
+                val cls = findClassInClassLoader(erasedName) ?: return null
+                val packageName = cls.`package`.name
+
+                val packageItem =
+                    codebase.findPackage(packageName)
+                        ?: TextPackageItem(
+                                codebase = codebase,
+                                name = packageName,
+                                modifiers = DefaultModifierList(codebase),
+                                position = SourcePositionInfo.UNKNOWN,
+                            )
+                            .also { newPackageItem -> codebase.addPackage(newPackageItem) }
+
+                TextClassItem(
+                        codebase = codebase,
+                        modifiers = DefaultModifierList(codebase),
+                        qualifiedName = cls.canonicalName,
+                    )
+                    .also { newClassItem ->
+                        codebase.registerClass(newClassItem)
+                        packageItem.addClass(newClassItem)
+                    }
+            }
+    }
+}
diff --git a/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextTypeItemTest.kt b/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextTypeItemTest.kt
deleted file mode 100644
index ee63d91..0000000
--- a/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextTypeItemTest.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tools.metalava.model.text
-
-import com.android.tools.metalava.model.Assertions
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-
-class TextTypeItemTest : Assertions {
-    @Test
-    fun `check bounds`() {
-        // When a type variable is on a member and the type variable is defined on the surrounding
-        // class, look up the bound on the class type parameter:
-        val codebase =
-            ApiFile.parseApi(
-                "test",
-                """
-            // Signature format: 2.0
-            package androidx.navigation {
-              public final class NavDestination {
-                ctor public NavDestination();
-              }
-              public class NavDestinationBuilder<D extends androidx.navigation.NavDestination> {
-                ctor public NavDestinationBuilder(int id);
-                method public D build();
-              }
-            }
-            """
-                    .trimIndent(),
-            )
-        val cls = codebase.assertClass("androidx.navigation.NavDestinationBuilder")
-        val method = cls.assertMethod("build", "") as TextMethodItem
-
-        assertThat(TextTypeParameterItem.bounds("D", method).toString())
-            .isEqualTo("[androidx.navigation.NavDestination]")
-    }
-
-    @Test
-    fun `check implicit bounds from object`() {
-        // When a type variable is on a member and the type variable is defined on the surrounding
-        // class, look up the bound on the class type parameter:
-        val codebase =
-            ApiFile.parseApi(
-                "test",
-                """
-            // Signature format: 2.0
-            package test.pkg {
-              public final class TestClass<D> {
-                method public D build();
-              }
-            }
-            """
-                    .trimIndent(),
-            )
-        val cls = codebase.assertClass("test.pkg.TestClass") as TextClassItem
-        val method = cls.assertMethod("build", "") as TextMethodItem
-
-        // The implicit upper bound of `java.lang.Object` that is used for any type parameter that
-        // does not explicitly define a bound is not included in `bounds`.
-        assertThat(TextTypeParameterItem.bounds("D", method)).isEqualTo(emptyList<String>())
-    }
-
-    @Test
-    fun `check bounds from enums`() {
-        // When a type variable is on a member and the type variable is defined on the surrounding
-        // class, look up the bound on the class type parameter:
-        val codebase =
-            ApiFile.parseApi(
-                "test",
-                """
-            // Signature format: 2.0
-            package test.pkg {
-              public class EnumMap<K extends java.lang.Enum<K>, V> extends java.util.AbstractMap implements java.lang.Cloneable java.io.Serializable {
-                method public java.util.EnumMap<K, V> clone();
-                method public java.util.Set<java.util.Map.Entry<K, V>> entrySet();
-              }
-            }
-            """
-                    .trimIndent(),
-            )
-        val cls = codebase.assertClass("test.pkg.EnumMap")
-        val method = cls.assertMethod("clone", "") as TextMethodItem
-
-        assertThat(TextTypeParameterItem.bounds("K", method)).isEqualTo(listOf("java.lang.Enum<K>"))
-    }
-}
diff --git a/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextTypeParameterItemTest.kt b/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextTypeParameterItemTest.kt
deleted file mode 100644
index 37c44f6..0000000
--- a/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextTypeParameterItemTest.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tools.metalava.model.text
-
-import com.android.tools.metalava.model.Assertions
-import com.android.tools.metalava.model.text.TextTypeParameterItem.Companion.bounds
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-
-class TextTypeParameterItemTest : Assertions {
-    @Test
-    fun testTypeParameterNames() {
-        assertThat(bounds(null).toString()).isEqualTo("[]")
-        assertThat(bounds("").toString()).isEqualTo("[]")
-        assertThat(bounds("X").toString()).isEqualTo("[]")
-        assertThat(bounds("DEF extends T").toString()).isEqualTo("[T]")
-        assertThat(bounds("T extends java.lang.Comparable<? super T>").toString())
-            .isEqualTo("[java.lang.Comparable<? super T>]")
-        assertThat(bounds("T extends java.util.List<Number> & java.util.RandomAccess").toString())
-            .isEqualTo("[java.util.List<Number>, java.util.RandomAccess]")
-
-        // When a type variable is on a member and the type variable is defined on the surrounding
-        // class, look up the bound on the class type parameter:
-        val codebase =
-            ApiFile.parseApi(
-                "test",
-                """
-            // Signature format: 2.0
-            package androidx.navigation {
-              public final class NavDestination {
-                ctor public NavDestination();
-              }
-              public class NavDestinationBuilder<D extends androidx.navigation.NavDestination> {
-                ctor public NavDestinationBuilder(int id);
-                method public D build();
-              }
-            }
-            """
-                    .trimIndent(),
-            )
-        val cls = codebase.assertClass("androidx.navigation.NavDestinationBuilder")
-        val method = cls.assertMethod("build", "") as TextMethodItem
-        assertThat(method).isNotNull()
-        assertThat(bounds("D", method).toString()).isEqualTo("[androidx.navigation.NavDestination]")
-    }
-}
diff --git a/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextTypeParserCacheTest.kt b/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextTypeParserCacheTest.kt
new file mode 100644
index 0000000..e71d115
--- /dev/null
+++ b/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextTypeParserCacheTest.kt
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.model.text
+
+import com.android.tools.metalava.model.ArrayTypeItem
+import com.android.tools.metalava.model.ClassTypeItem
+import com.android.tools.metalava.model.TypeItem
+import com.android.tools.metalava.model.VariableTypeItem
+import com.android.tools.metalava.model.WildcardTypeItem
+import com.android.tools.metalava.testing.getAndroidTxt
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Test
+
+/** Test the behavior of [TextTypeParser]#s caching. */
+class TextTypeParserCacheTest : BaseTextCodebaseTest() {
+
+    private data class Context(
+        val codebase: TextCodebase,
+        val parser: TextTypeParser,
+        val emptyScope: TypeParameterScope,
+        val nonEmptyScope: TypeParameterScope,
+    )
+
+    private fun runTextTypeParserTest(test: Context.() -> Unit) {
+        runSignatureTest(
+            signature(
+                """
+                    // Signature format: 2.0
+                    package test.pkg {
+                        public class Generic<T> {
+                        }
+                    }
+                """
+            ),
+        ) {
+            val textCodebase = codebase as TextCodebase
+            val parser =
+                TextTypeParser(
+                    textCodebase,
+                    kotlinStyleNulls = false,
+                )
+            val nonEmptyScope = TypeParameterScope.from(codebase.assertClass("test.pkg.Generic"))
+            val context =
+                Context(
+                    textCodebase,
+                    parser,
+                    TypeParameterScope.empty,
+                    nonEmptyScope,
+                )
+            context.test()
+        }
+    }
+
+    @Test
+    fun `Test loading previously released public API`() {
+        val androidTxtFiles =
+            listOf("public", "system", "module-lib").map { surface -> getAndroidTxt(34, surface) }
+        ApiFile.parseApi(
+            androidTxtFiles,
+            apiStatsConsumer = { stats ->
+                assertThat(stats)
+                    .isEqualTo(
+                        ApiFile.Stats(
+                            totalClasses = 7315,
+                            typeCacheRequests = 179041,
+                            typeCacheSkip = 9383,
+                            typeCacheHit = 161407,
+                            typeCacheSize = 8251,
+                        )
+                    )
+            }
+        )
+    }
+
+    @Test
+    fun `Test empty scope is cached`() {
+        runTextTypeParserTest {
+            val first = parser.obtainTypeFromString("int", emptyScope)
+            val second = parser.obtainTypeFromString("int", emptyScope)
+
+            assertThat(first).isSameInstanceAs(second)
+        }
+    }
+
+    @Test
+    fun `Test non-empty scope is not cached but could be`() {
+        runTextTypeParserTest {
+            val first = parser.obtainTypeFromString("int", nonEmptyScope)
+            val second = parser.obtainTypeFromString("int", nonEmptyScope)
+
+            assertThat(first).isNotSameInstanceAs(second)
+        }
+    }
+
+    @Test
+    fun `Test type that references a type parameter is not cached`() {
+        runTextTypeParserTest {
+            val first = parser.obtainTypeFromString("T", nonEmptyScope)
+            val second = parser.obtainTypeFromString("T", nonEmptyScope)
+
+            assertThat(first).isNotSameInstanceAs(second)
+        }
+    }
+
+    @Test
+    fun `Test caching of type variables`() {
+        runSignatureTest(
+            signature(
+                """
+                    // Signature format: 4.0
+                    package test.pkg {
+                      public class Foo<A> {
+                        method public <B extends java.lang.String> void bar1(B p0);
+                        method public <B extends java.lang.String> void bar2(B p0);
+                        method public <C> void bar3(java.util.List<C> p0);
+                        method public <C> void bar4(java.util.List<C> p0);
+                      }
+                    }
+                """
+            ),
+        ) {
+            val foo = codebase.assertClass("test.pkg.Foo")
+            assertThat(foo.methods()).hasSize(4)
+
+            val bar1Param = foo.methods()[0].parameters()[0].type()
+            val bar2Param = foo.methods()[1].parameters()[0].type()
+
+            // The type variable should not be reused between methods
+            assertThat(bar1Param).isNotSameInstanceAs(bar2Param)
+
+            val bar3Param = foo.methods()[2].parameters()[0].type()
+            val bar4Param = foo.methods()[3].parameters()[0].type()
+
+            // The type referencing a type variable should not be reused between methods
+            assertThat(bar3Param).isNotSameInstanceAs(bar4Param)
+        }
+    }
+
+    @Test
+    fun `Test caching of type variables collide with String`() {
+        runSignatureTest(
+            signature(
+                """
+                    // Signature format: 4.0
+                    package test.pkg {
+                      public class Foo {
+                        method public void bar1(String);
+                        method public <String> void bar2(String);
+                        method public void bar3(String);
+                      }
+                    }
+                """
+            ),
+        ) {
+            val foo = codebase.assertClass("test.pkg.Foo")
+
+            // Get the type of the parameter of all the methods.
+            val (bar1Param, bar2Param, bar3Param) = foo.methods().map { it.parameters()[0].type() }
+
+            // Even though all the method's parameter types are the same string representation they
+            // have two different types.
+            assertThat(bar1Param).isInstanceOf(ClassTypeItem::class.java)
+            assertThat(bar2Param).isInstanceOf(VariableTypeItem::class.java)
+            assertThat(bar3Param).isInstanceOf(ClassTypeItem::class.java)
+
+            assertThat(bar1Param).isSameInstanceAs(bar3Param)
+        }
+    }
+
+    @Test
+    fun `Test caching of array components`() {
+        runTextTypeParserTest {
+            val first = parser.obtainTypeFromString("String", emptyScope)
+            val second = parser.obtainTypeFromString("String[]", emptyScope) as ArrayTypeItem
+            val third = parser.obtainTypeFromString("String[][]", emptyScope) as ArrayTypeItem
+
+            assertWithMessage("String === [] of String[]")
+                .that(second.componentType)
+                .isSameInstanceAs(first)
+            assertWithMessage("String === [][] of String[][]")
+                .that((third.componentType as ArrayTypeItem).componentType)
+                .isSameInstanceAs(first)
+            assertWithMessage("String[] !== [] of String[][]")
+                .that(third.componentType)
+                .isNotSameInstanceAs(second)
+        }
+    }
+
+    @Test
+    fun `Test caching of array with component annotations`() {
+        runTextTypeParserTest {
+            fun arrayTypeItem(type: String) =
+                parser.obtainTypeFromString(type, emptyScope) as ArrayTypeItem
+
+            val noAnno = arrayTypeItem("String[]")
+            val withAnno1 = arrayTypeItem("@Anno1 String[]")
+            val withAnno2 = arrayTypeItem("@Anno2 String[]")
+            val withAnno1Again = arrayTypeItem("@Anno1 String[]")
+            val withAnno1TwoDims = arrayTypeItem("@Anno1 String[][]")
+
+            // Type without an annotation can never match a type with as annotation.
+            for (withAnno in listOf(withAnno1, withAnno2, withAnno1Again)) {
+                assertWithMessage("$noAnno not same as $withAnno")
+                    .that(noAnno.componentType)
+                    .isNotSameInstanceAs(withAnno1)
+            }
+
+            // Type with one annotation can never match a type with a different annotation.
+            for (withAnno in listOf(withAnno1, withAnno1Again)) {
+                assertWithMessage("$withAnno2 not same as $withAnno")
+                    .that(noAnno.componentType)
+                    .isNotSameInstanceAs(withAnno1)
+            }
+
+            // The exact same top level type are the same.
+            assertWithMessage("withAnno1 and withAnno1Again")
+                .that(withAnno1)
+                .isSameInstanceAs(withAnno1Again)
+
+            // Check the deepest components of withAnno1 and withAnnot1TwoDIms
+            fun TypeItem.deepestComponent(): TypeItem =
+                if (this is ArrayTypeItem) componentType.deepestComponent() else this
+
+            // Their strings representations are the same.
+            assertWithMessage(
+                    "string representation of withAnno1.deepestComponent() and withAnno1TwoDims.deepestComponent()"
+                )
+                .that(withAnno1TwoDims.deepestComponent().toTypeString(annotations = true))
+                .isEqualTo(withAnno1.deepestComponent().toTypeString(annotations = true))
+
+            // But they are different instances as types with annotations are not cached..
+            assertWithMessage(
+                    "identity of withAnno1.deepestComponent() and withAnno1TwoDims.deepestComponent()"
+                )
+                .that(withAnno1TwoDims.deepestComponent())
+                .isNotSameInstanceAs(withAnno1.deepestComponent())
+        }
+    }
+
+    @Test
+    fun `Test caching of generic type arguments`() {
+        runTextTypeParserTest {
+            val first = parser.obtainTypeFromString("Number", emptyScope)
+            val second = parser.obtainTypeFromString("List<Number>", emptyScope) as ClassTypeItem
+
+            assertThat(second.arguments[0]).isSameInstanceAs(first)
+        }
+    }
+
+    @Test
+    fun `Test caching of wildcard extends bounds`() {
+        runTextTypeParserTest {
+            val first = parser.obtainTypeFromString("Number", emptyScope)
+            val second =
+                parser.obtainTypeFromString("List<? extends Number>", emptyScope) as ClassTypeItem
+
+            assertThat((second.arguments[0] as WildcardTypeItem).extendsBound)
+                .isSameInstanceAs(first)
+        }
+    }
+
+    @Test
+    fun `Test caching of wildcard super bounds`() {
+        runTextTypeParserTest {
+            val first = parser.obtainTypeFromString("Number", emptyScope)
+            val second =
+                parser.obtainTypeFromString("List<? super Number>", emptyScope) as ClassTypeItem
+
+            assertThat((second.arguments[0] as WildcardTypeItem).superBound).isSameInstanceAs(first)
+        }
+    }
+}
diff --git a/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextTypeParserTest.kt b/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextTypeParserTest.kt
index 72b8b17..1cf4e22 100644
--- a/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextTypeParserTest.kt
+++ b/metalava-model-text/src/test/java/com/android/tools/metalava/model/text/TextTypeParserTest.kt
@@ -17,7 +17,6 @@
 package com.android.tools.metalava.model.text
 
 import com.android.tools.metalava.model.ArrayTypeItem
-import com.android.tools.metalava.model.Assertions
 import com.android.tools.metalava.model.ClassTypeItem
 import com.android.tools.metalava.model.TypeItem
 import com.android.tools.metalava.model.TypeNullability
@@ -25,7 +24,7 @@
 import org.junit.Assert
 import org.junit.Test
 
-class TextTypeParserTest : Assertions {
+class TextTypeParserTest : BaseTextCodebaseTest() {
     @Test
     fun `Test type parameter strings`() {
         assertThat(TextTypeParser.typeParameterStrings(null).toString()).isEqualTo("[]")
@@ -81,40 +80,6 @@
     }
 
     @Test
-    fun `Test caching of type variables`() {
-        val codebase =
-            ApiFile.parseApi(
-                "test",
-                """
-                    // Signature format: 4.0
-                    package test.pkg {
-                      public class Foo<A> {
-                        method public void bar1<B extends java.lang.String>(B p0);
-                        method public void bar2<B extends java.lang.String>(B p0);
-                        method public void bar3<C>(java.util.List<C> p0);
-                        method public void bar4<C>(java.util.List<C> p0);
-                      }
-                    }
-                """
-                    .trimIndent()
-            )
-        val foo = codebase.assertClass("test.pkg.Foo")
-        assertThat(foo.methods()).hasSize(4)
-
-        val bar1Param = foo.methods()[0].parameters()[0].type()
-        val bar2Param = foo.methods()[1].parameters()[0].type()
-
-        // The type variable should not be reused between methods
-        assertThat(bar1Param).isNotSameInstanceAs(bar2Param)
-
-        val bar3Param = foo.methods()[2].parameters()[0].type()
-        val bar4Param = foo.methods()[3].parameters()[0].type()
-
-        // The type referencing a type variable should not be reused between methods
-        assertThat(bar3Param).isNotSameInstanceAs(bar4Param)
-    }
-
-    @Test
     fun `Test splitting Kotlin nullability suffix`() {
         assertThat(TextTypeParser.splitNullabilitySuffix("String!", true))
             .isEqualTo(Pair("String", TypeNullability.PLATFORM))
@@ -413,9 +378,10 @@
         )
     }
 
-    private val typeParser = TextTypeParser(ApiFile.parseApi("test", ""))
+    private val typeParser = TextTypeParser(ApiFile.parseApi("test", "") as TextCodebase)
 
-    private fun parseType(type: String) = typeParser.obtainTypeFromString(type)
+    private fun parseType(type: String) =
+        typeParser.obtainTypeFromString(type, TypeParameterScope.empty)
 
     /**
      * Tests that [inputType] is parsed as an [ArrayTypeItem] with component type equal to
@@ -453,17 +419,17 @@
 
     /**
      * Tests that [inputType] is parsed as a [ClassTypeItem] with qualified name equal to
-     * [expectedQualifiedName] and parameters equal to [expectedParameterTypes].
+     * [expectedQualifiedName] and [ClassTypeItem.arguments] is equal to [expectedTypeArguments].
      */
     private fun testClassType(
         inputType: String,
         expectedQualifiedName: String,
-        expectedParameterTypes: List<TypeItem>
+        expectedTypeArguments: List<TypeItem>
     ) {
         val type = parseType(inputType)
         assertThat(type).isInstanceOf(ClassTypeItem::class.java)
         assertThat((type as ClassTypeItem).qualifiedName).isEqualTo(expectedQualifiedName)
-        assertThat((type as ClassTypeItem).parameters).isEqualTo(expectedParameterTypes)
+        assertThat((type as ClassTypeItem).arguments).isEqualTo(expectedTypeArguments)
     }
 
     @Test
@@ -471,7 +437,7 @@
         testClassType(
             inputType = "String",
             expectedQualifiedName = "java.lang.String",
-            expectedParameterTypes = emptyList()
+            expectedTypeArguments = emptyList()
         )
         testArrayType(
             inputType = "String[]",
@@ -490,27 +456,27 @@
         testClassType(
             inputType = "@A @B test.pkg.Foo",
             expectedQualifiedName = "test.pkg.Foo",
-            expectedParameterTypes = emptyList()
+            expectedTypeArguments = emptyList()
         )
         testClassType(
             inputType = "@A @B test.pkg.Foo",
             expectedQualifiedName = "test.pkg.Foo",
-            expectedParameterTypes = emptyList()
+            expectedTypeArguments = emptyList()
         )
         testClassType(
             inputType = "java.lang.annotation.@NonNull Annotation",
             expectedQualifiedName = "java.lang.annotation.Annotation",
-            expectedParameterTypes = emptyList()
+            expectedTypeArguments = emptyList()
         )
         testClassType(
             inputType = "java.util.Map.@NonNull Entry<a.A,b.B>",
             expectedQualifiedName = "java.util.Map.Entry",
-            expectedParameterTypes = listOf(parseType("a.A"), parseType("b.B"))
+            expectedTypeArguments = listOf(parseType("a.A"), parseType("b.B"))
         )
         testClassType(
             inputType = "java.util.@NonNull Set<java.util.Map.@NonNull Entry<a.A,b.B>>",
             expectedQualifiedName = "java.util.Set",
-            expectedParameterTypes = listOf(parseType("java.util.Map.@NonNull Entry<a.A,b.B>"))
+            expectedTypeArguments = listOf(parseType("java.util.Map.@NonNull Entry<a.A,b.B>"))
         )
     }
 }
diff --git a/metalava-model-text/src/test/resources/model-test-suite-baseline.txt b/metalava-model-text/src/test/resources/model-test-suite-baseline.txt
index b398b52..b8cbb5f 100644
--- a/metalava-model-text/src/test/resources/model-test-suite-baseline.txt
+++ b/metalava-model-text/src/test/resources/model-test-suite-baseline.txt
@@ -6,11 +6,15 @@
   annotation toSource() with enum values[text,signature]
   annotation toSource() with number values[text,signature]
   annotation toSource() with string values[text,signature]
+  annotation with constant literal values[text,signature]
   annotation with infinity values[text,signature]
-  annotation with negative values[text,signature]
+  annotation with negative number values[text,signature]
   annotation with type cast values[text,signature]
 
 com.android.tools.metalava.model.testsuite.typeitem.CommonTypeModifiersTest
   Test implicit nullability of annotation members[text,signature]
   Test implicit nullability of equals parameter[text,signature]
   Test implicit nullability of toString[text,signature]
+
+com.android.tools.metalava.model.testsuite.typeitem.CommonTypeParameterItemTest
+  Test type parameter with annotations[text,signature]
diff --git a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineAnnotationItem.kt b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineAnnotationItem.kt
index 365ae1f..47c0b4c 100644
--- a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineAnnotationItem.kt
+++ b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineAnnotationItem.kt
@@ -19,7 +19,7 @@
 import com.android.tools.metalava.model.AnnotationAttribute
 import com.android.tools.metalava.model.DefaultAnnotationItem
 
-class TurbineAnnotationItem
+internal class TurbineAnnotationItem
 constructor(
     override val codebase: TurbineBasedCodebase,
     originalName: String?,
diff --git a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineBasedCodebase.kt b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineBasedCodebase.kt
index 91554ca..ff4d927 100644
--- a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineBasedCodebase.kt
+++ b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineBasedCodebase.kt
@@ -33,7 +33,7 @@
 const val PACKAGE_ESTIMATE = 500
 const val CLASS_ESTIMATE = 15000
 
-open class TurbineBasedCodebase(
+internal open class TurbineBasedCodebase(
     location: File,
     description: String = "Unknown",
     annotationManager: AnnotationManager,
@@ -70,6 +70,8 @@
         return classMap[className]
     }
 
+    override fun resolveClass(className: String) = findOrCreateClass(className)
+
     fun findOrCreateClass(className: String): TurbineClassItem? {
         return initializer.findOrCreateClass(className)
     }
diff --git a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineClassItem.kt b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineClassItem.kt
index dc52eff..3d4b7a2 100644
--- a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineClassItem.kt
+++ b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineClassItem.kt
@@ -18,25 +18,26 @@
 
 import com.android.tools.metalava.model.AnnotationRetention
 import com.android.tools.metalava.model.ClassItem
+import com.android.tools.metalava.model.ClassKind
+import com.android.tools.metalava.model.ClassTypeItem
 import com.android.tools.metalava.model.ConstructorItem
 import com.android.tools.metalava.model.FieldItem
 import com.android.tools.metalava.model.MethodItem
 import com.android.tools.metalava.model.PackageItem
 import com.android.tools.metalava.model.PropertyItem
 import com.android.tools.metalava.model.SourceFile
-import com.android.tools.metalava.model.TypeItem
 import com.android.tools.metalava.model.TypeParameterList
 import com.google.turbine.binder.sym.ClassSymbol
 import com.google.turbine.binder.sym.MethodSymbol
 
-open class TurbineClassItem(
+internal open class TurbineClassItem(
     codebase: TurbineBasedCodebase,
     private val name: String,
     private val fullName: String,
     private val qualifiedName: String,
     private val classSymbol: ClassSymbol,
     modifiers: TurbineModifierItem,
-    private val classType: TurbineClassType,
+    override val classKind: ClassKind,
     private val typeParameters: TypeParameterList,
     documentation: String,
     private val source: SourceFile?
@@ -46,15 +47,13 @@
 
     override var hasPrivateConstructor: Boolean = false
 
-    override val isTypeParameter: Boolean = false
-
     override var stubConstructor: ConstructorItem? = null
 
     internal lateinit var innerClasses: List<TurbineClassItem>
 
     private var superClass: TurbineClassItem? = null
 
-    private var superClassType: TypeItem? = null
+    private var superClassType: ClassTypeItem? = null
 
     internal lateinit var directInterfaces: List<TurbineClassItem>
 
@@ -64,15 +63,15 @@
 
     internal lateinit var fields: List<TurbineFieldItem>
 
-    internal lateinit var methods: List<TurbineMethodItem>
+    internal lateinit var methods: MutableList<TurbineMethodItem>
 
     internal lateinit var constructors: List<TurbineConstructorItem>
 
     internal var containingClass: TurbineClassItem? = null
 
-    private lateinit var interfaceTypesList: List<TypeItem>
+    private lateinit var interfaceTypesList: List<ClassTypeItem>
 
-    private var asType: TurbineTypeItem? = null
+    private var asType: TurbineClassTypeItem? = null
 
     internal var hasImplicitDefaultConstructor = false
 
@@ -136,18 +135,12 @@
 
     override fun innerClasses(): List<ClassItem> = innerClasses
 
-    override fun interfaceTypes(): List<TypeItem> = interfaceTypesList
-
-    override fun isAnnotationType(): Boolean = classType == TurbineClassType.ANNOTATION
+    override fun interfaceTypes(): List<ClassTypeItem> = interfaceTypesList
 
     override fun isDefined(): Boolean {
         TODO("b/295800205")
     }
 
-    override fun isEnum(): Boolean = classType == TurbineClassType.ENUM
-
-    override fun isInterface(): Boolean = classType == TurbineClassType.INTERFACE
-
     override fun methods(): List<MethodItem> = methods
 
     /**
@@ -162,27 +155,27 @@
 
     override fun fullName(): String = fullName
 
-    override fun setInterfaceTypes(interfaceTypes: List<TypeItem>) {
+    override fun setInterfaceTypes(interfaceTypes: List<ClassTypeItem>) {
         interfaceTypesList = interfaceTypes
     }
 
-    internal fun setSuperClass(superClass: ClassItem?, superClassType: TypeItem?) {
+    internal fun setSuperClass(superClass: ClassItem?, superClassType: ClassTypeItem?) {
         this.superClass = superClass as? TurbineClassItem
         this.superClassType = superClassType
     }
 
     override fun superClass(): TurbineClassItem? = superClass
 
-    override fun superClassType(): TypeItem? = superClassType
+    override fun superClassType(): ClassTypeItem? = superClassType
 
-    override fun toType(): TurbineTypeItem {
+    override fun type(): TurbineClassTypeItem {
         if (asType == null) {
             val parameters =
                 typeParameterList().typeParameters().map {
                     createVariableType(it as TurbineTypeParameterItem)
                 }
             val mods = TurbineTypeModifiers(modifiers.annotations())
-            val outerClassType = containingClass?.let { it.toType() as TurbineClassTypeItem }
+            val outerClassType = containingClass?.type()
             asType = TurbineClassTypeItem(codebase, mods, qualifiedName, parameters, outerClassType)
         }
         return asType!!
@@ -205,4 +198,34 @@
     }
 
     override fun getSourceFile(): SourceFile? = source
+
+    override fun inheritMethodFromNonApiAncestor(template: MethodItem): MethodItem {
+        val method = template as TurbineMethodItem
+        val replacementMap = mapTypeVariables(method.containingClass())
+        val retType = method.returnType().convertType(replacementMap)
+        val mods = method.modifiers.duplicate()
+        val params =
+            method.parameters().map { TurbineParameterItem.duplicate(codebase, it, replacementMap) }
+
+        val duplicateMethod =
+            TurbineMethodItem(
+                codebase,
+                method.getSymbol(),
+                this,
+                retType as TurbineTypeItem,
+                mods,
+                method.typeParameterList(),
+                method.documentation
+            )
+        mods.setOwner(duplicateMethod)
+        duplicateMethod.parameters = params
+        duplicateMethod.inheritedFrom = method.containingClass()
+        duplicateMethod.setThrowsTypes(method.throwsTypes())
+
+        return duplicateMethod
+    }
+
+    override fun addMethod(method: MethodItem) {
+        methods.add(method as TurbineMethodItem)
+    }
 }
diff --git a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineClassType.kt b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineClassType.kt
deleted file mode 100644
index ae6a705..0000000
--- a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineClassType.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tools.metalava.model.turbine
-
-import com.google.turbine.model.TurbineTyKind
-
-enum class TurbineClassType() {
-    INTERFACE,
-    ENUM,
-    ANNOTATION,
-    TYPE_PARAMETER,
-    CLASS;
-
-    companion object {
-        fun getClassType(type: TurbineTyKind): TurbineClassType {
-            return when (type) {
-                TurbineTyKind.INTERFACE -> INTERFACE
-                TurbineTyKind.ENUM -> ENUM
-                TurbineTyKind.ANNOTATION -> ANNOTATION
-                else -> CLASS
-            }
-        }
-    }
-}
diff --git a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineCodebaseInitialiser.kt b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineCodebaseInitialiser.kt
index 9053ff4..ff322da 100644
--- a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineCodebaseInitialiser.kt
+++ b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineCodebaseInitialiser.kt
@@ -16,12 +16,15 @@
 
 package com.android.tools.metalava.model.turbine
 
+import com.android.tools.metalava.model.ANNOTATION_ATTR_VALUE
 import com.android.tools.metalava.model.AnnotationAttribute
 import com.android.tools.metalava.model.AnnotationAttributeValue
 import com.android.tools.metalava.model.AnnotationItem
 import com.android.tools.metalava.model.ArrayTypeItem
 import com.android.tools.metalava.model.BaseItemVisitor
+import com.android.tools.metalava.model.BoundsTypeItem
 import com.android.tools.metalava.model.ClassItem
+import com.android.tools.metalava.model.ClassKind
 import com.android.tools.metalava.model.ClassTypeItem
 import com.android.tools.metalava.model.DefaultAnnotationArrayAttributeValue
 import com.android.tools.metalava.model.DefaultAnnotationAttribute
@@ -29,15 +32,19 @@
 import com.android.tools.metalava.model.Item
 import com.android.tools.metalava.model.MethodItem
 import com.android.tools.metalava.model.PrimitiveTypeItem.Primitive
+import com.android.tools.metalava.model.ReferenceTypeItem
+import com.android.tools.metalava.model.TypeArgumentTypeItem
 import com.android.tools.metalava.model.TypeItem
 import com.android.tools.metalava.model.TypeNullability
 import com.android.tools.metalava.model.TypeParameterList
+import com.android.tools.metalava.model.TypeUse
 import com.google.common.collect.ImmutableList
 import com.google.common.collect.ImmutableMap
 import com.google.turbine.binder.Binder
 import com.google.turbine.binder.Binder.BindingResult
 import com.google.turbine.binder.ClassPathBinder
 import com.google.turbine.binder.Processing.ProcessorInfo
+import com.google.turbine.binder.bound.EnumConstantValue
 import com.google.turbine.binder.bound.SourceTypeBoundClass
 import com.google.turbine.binder.bound.TurbineClassValue
 import com.google.turbine.binder.bound.TypeBoundClass
@@ -58,7 +65,10 @@
 import com.google.turbine.model.Const.Value
 import com.google.turbine.model.TurbineConstantTypeKind as PrimKind
 import com.google.turbine.model.TurbineFlag
+import com.google.turbine.model.TurbineTyKind
 import com.google.turbine.tree.Tree
+import com.google.turbine.tree.Tree.ArrayInit
+import com.google.turbine.tree.Tree.Assign
 import com.google.turbine.tree.Tree.CompUnit
 import com.google.turbine.tree.Tree.Expression
 import com.google.turbine.tree.Tree.Ident
@@ -85,7 +95,7 @@
  * This is used for populating all the classes,packages and other items from the data present in the
  * parsed Tree
  */
-open class TurbineCodebaseInitialiser(
+internal open class TurbineCodebaseInitialiser(
     val units: List<CompUnit>,
     val codebase: TurbineBasedCodebase,
     val classpath: List<File>,
@@ -295,7 +305,7 @@
                 qualifiedName,
                 sym,
                 modifierItem,
-                TurbineClassType.getClassType(cls.kind()),
+                getClassKind(cls.kind()),
                 typeParameters,
                 getCommentedDoc(documentation),
                 sourceFile,
@@ -309,7 +319,7 @@
                 cls.superclass()?.let { superClass -> findOrCreateClass(superClass) }
             val superClassType = cls.superClassType()
             val superClassTypeItem =
-                if (superClassType == null) null else createType(superClassType, false)
+                if (superClassType == null) null else createSuperType(superClassType)
             classItem.setSuperClass(superClassItem, superClassTypeItem)
         }
 
@@ -317,7 +327,7 @@
         classItem.directInterfaces = cls.interfaces().map { itf -> findOrCreateClass(itf) }
 
         // Set interface types
-        classItem.setInterfaceTypes(cls.interfaceTypes().map { createType(it, false) })
+        classItem.setInterfaceTypes(cls.interfaceTypes().map { createSuperType(it) })
 
         // Create fields
         createFields(classItem, cls.fields())
@@ -346,26 +356,35 @@
             classItem.emit = false
         }
 
+        // Create InnerClasses.
+        val children = cls.children()
+        createInnerClasses(classItem, children.values.asList())
+
         // Set the throwslist for methods
         classItem.methods.forEach { it.setThrowsTypes() }
 
         // Set the throwslist for constructors
         classItem.constructors.forEach { it.setThrowsTypes() }
 
-        // Create InnerClasses.
-        val children = cls.children()
-        createInnerClasses(classItem, children.values.asList())
-
         return classItem
     }
 
+    fun getClassKind(type: TurbineTyKind): ClassKind {
+        return when (type) {
+            TurbineTyKind.INTERFACE -> ClassKind.INTERFACE
+            TurbineTyKind.ENUM -> ClassKind.ENUM
+            TurbineTyKind.ANNOTATION -> ClassKind.ANNOTATION_TYPE
+            else -> ClassKind.CLASS
+        }
+    }
+
     /** Creates a list of AnnotationItems from given list of Turbine Annotations */
     private fun createAnnotations(annotations: List<AnnoInfo>): List<AnnotationItem> {
         return annotations.mapNotNull { createAnnotation(it) }
     }
 
     private fun createAnnotation(annotation: AnnoInfo): TurbineAnnotationItem? {
-        val annoAttrs = getAnnotationAttributes(annotation.values())
+        val annoAttrs = getAnnotationAttributes(annotation.values(), annotation.tree()?.args())
 
         val nameList = annotation.tree()?.let { tree -> tree.name().map { it.value() } }
         val simpleName = nameList?.let { it -> it.joinToString(separator = ".") }
@@ -378,24 +397,119 @@
 
     /** Creates a list of AnnotationAttribute from the map of name-value attribute pairs */
     private fun getAnnotationAttributes(
-        attrs: ImmutableMap<String, Const>
+        attrs: ImmutableMap<String, Const>,
+        exprs: ImmutableList<Expression>?
     ): List<AnnotationAttribute> {
         val attributes = mutableListOf<AnnotationAttribute>()
-        for ((name, value) in attrs) {
-            attributes.add(DefaultAnnotationAttribute(name, createAttrValue(value)))
+        if (exprs != null) {
+            for (exp in exprs) {
+                when (exp.kind()) {
+                    Tree.Kind.ASSIGN -> {
+                        exp as Assign
+                        val name = exp.name().value()
+                        val assignExp = exp.expr()
+                        attributes.add(
+                            DefaultAnnotationAttribute(
+                                name,
+                                createAttrValue(attrs[name]!!, assignExp)
+                            )
+                        )
+                    }
+                    else -> {
+                        val name = ANNOTATION_ATTR_VALUE
+                        attributes.add(
+                            DefaultAnnotationAttribute(name, createAttrValue(attrs[name]!!, exp))
+                        )
+                    }
+                }
+            }
+        } else {
+            for ((name, value) in attrs) {
+                attributes.add(DefaultAnnotationAttribute(name, createAttrValue(value, null)))
+            }
         }
         return attributes
     }
 
-    private fun createAttrValue(const: Const): AnnotationAttributeValue {
+    private fun createAttrValue(const: Const, expr: Expression?): AnnotationAttributeValue {
         if (const.kind() == Kind.ARRAY) {
-            val arrayVal = const as ArrayInitValue
+            const as ArrayInitValue
+            if (const.elements().count() == 1 && expr != null && !(expr is ArrayInit)) {
+                // This is case where defined type is array type but provided attribute value is
+                // single non-array element
+                // For e.g. @Anno(5) where Anno is @interfacce Anno {int [] value()}
+                val constLiteral = const.elements().single()
+                return DefaultAnnotationSingleAttributeValue(
+                    { getSource(constLiteral, expr) },
+                    { getValue(constLiteral) }
+                )
+            }
             return DefaultAnnotationArrayAttributeValue(
-                { arrayVal.toString() },
-                { arrayVal.elements().map { createAttrValue(it) } }
+                { getSource(const, expr) },
+                { const.elements().map { createAttrValue(it, null) } }
             )
         }
-        return DefaultAnnotationSingleAttributeValue({ const.toString() }, { getValue(const) })
+        return DefaultAnnotationSingleAttributeValue(
+            { getSource(const, expr) },
+            { getValue(const) }
+        )
+    }
+
+    private fun getSource(const: Const, expr: Expression?): String {
+        return when (const.kind()) {
+            Kind.PRIMITIVE -> {
+                when ((const as Value).constantTypeKind()) {
+                    PrimKind.INT -> {
+                        val value = (const as Const.IntValue).value()
+                        if (value < 0 || (expr != null && expr.kind() == Tree.Kind.TYPE_CAST))
+                            "0x" + value.toUInt().toString(16) // Hex Value
+                        else value.toString()
+                    }
+                    PrimKind.SHORT -> {
+                        val value = (const as Const.ShortValue).value()
+                        if (value < 0) "0x" + value.toUInt().toString(16) else value.toString()
+                    }
+                    PrimKind.FLOAT -> {
+                        val value = (const as Const.FloatValue).value()
+                        when {
+                            value == Float.POSITIVE_INFINITY -> "java.lang.Float.POSITIVE_INFINITY"
+                            value == Float.NEGATIVE_INFINITY -> "java.lang.Float.NEGATIVE_INFINITY"
+                            value < 0 -> value.toString() + "F" // Handling negative values
+                            else -> value.toString() + "f" // Handling positive values
+                        }
+                    }
+                    PrimKind.DOUBLE -> {
+                        val value = (const as Const.DoubleValue).value()
+                        when {
+                            value == Double.POSITIVE_INFINITY ->
+                                "java.lang.Double.POSITIVE_INFINITY"
+                            value == Double.NEGATIVE_INFINITY ->
+                                "java.lang.Double.NEGATIVE_INFINITY"
+                            else -> const.toString()
+                        }
+                    }
+                    PrimKind.BYTE -> const.getValue().toString()
+                    else -> const.toString()
+                }
+            }
+            Kind.ARRAY -> {
+                const as ArrayInitValue
+                val pairs =
+                    if (expr != null) const.elements().zip((expr as ArrayInit).exprs())
+                    else const.elements().map { Pair(it, null) }
+                buildString {
+                        append("{")
+                        pairs.joinTo(this, ", ") { getSource(it.first, it.second) }
+                        append("}")
+                    }
+                    .toString()
+            }
+            Kind.ENUM_CONSTANT -> getValue(const).toString()
+            Kind.CLASS_LITERAL -> {
+                if (expr != null) expr.toString() else getValue(const).toString()
+            }
+            else -> const.toString()
+        }
     }
 
     private fun getValue(const: Const): Any? {
@@ -409,11 +523,30 @@
                 val value = const as TurbineClassValue
                 return value.type().toString()
             }
-            else -> return const.toString()
+            Kind.ENUM_CONSTANT -> {
+                val value = const as EnumConstantValue
+                val temp =
+                    getQualifiedName(value.sym().owner().binaryName()) + "." + value.toString()
+                return temp
+            }
+            else -> {
+                return const.toString()
+            }
         }
     }
 
-    private fun createType(type: Type, isVarArg: Boolean): TurbineTypeItem {
+    /**
+     * Creates a [ClassTypeItem] that is suitable for use as a super type, e.g. in an `extends` or
+     * `implements` list.
+     */
+    private fun createSuperType(type: Type): ClassTypeItem =
+        createType(type, false, TypeUse.SUPER_TYPE) as ClassTypeItem
+
+    private fun createType(
+        type: Type,
+        isVarArg: Boolean,
+        typeUse: TypeUse = TypeUse.GENERAL,
+    ): TurbineTypeItem {
         return when (val kind = type.tyKind()) {
             TyKind.PRIM_TY -> {
                 type as PrimTy
@@ -447,7 +580,7 @@
                 for (simpleClass in type.classes()) {
                     // For all outer class types, set the nullability to non-null.
                     outerClass?.modifiers?.setNullability(TypeNullability.NONNULL)
-                    outerClass = createSimpleClassType(simpleClass, outerClass)
+                    outerClass = createSimpleClassType(simpleClass, outerClass, typeUse)
                 }
                 outerClass!!
             }
@@ -464,18 +597,18 @@
                 val modifiers = TurbineTypeModifiers(annotations, TypeNullability.UNDEFINED)
                 when (type.boundKind()) {
                     BoundKind.UPPER -> {
-                        val upperBound = createType(type.bound(), false)
+                        val upperBound = createWildcardBound(type.bound())
                         TurbineWildcardTypeItem(codebase, modifiers, upperBound, null)
                     }
                     BoundKind.LOWER -> {
                         // LowerBounded types have java.lang.Object as upper bound
-                        val upperBound = createType(ClassTy.OBJECT, false)
-                        val lowerBound = createType(type.bound(), false)
+                        val upperBound = createWildcardBound(ClassTy.OBJECT)
+                        val lowerBound = createWildcardBound(type.bound())
                         TurbineWildcardTypeItem(codebase, modifiers, upperBound, lowerBound)
                     }
                     BoundKind.NONE -> {
                         // Unbounded types have java.lang.Object as upper bound
-                        val upperBound = createType(ClassTy.OBJECT, false)
+                        val upperBound = createWildcardBound(ClassTy.OBJECT)
                         TurbineWildcardTypeItem(codebase, modifiers, upperBound, null)
                     }
                     else ->
@@ -511,6 +644,8 @@
         }
     }
 
+    private fun createWildcardBound(type: Type) = createType(type, false) as ReferenceTypeItem
+
     private fun createArrayType(type: ArrayTy, isVarArg: Boolean): TurbineTypeItem {
         // For Turbine's ArrayTy, the annotations for multidimentional arrays comes out in reverse
         // order. This method attaches annotations in the correct order by applying them in reverse
@@ -545,12 +680,15 @@
 
     private fun createSimpleClassType(
         type: SimpleClassTy,
-        outerClass: TurbineClassTypeItem?
+        outerClass: TurbineClassTypeItem?,
+        typeUse: TypeUse = TypeUse.GENERAL,
     ): TurbineClassTypeItem {
+        // Super types are always NONNULL.
+        val nullability = if (typeUse == TypeUse.SUPER_TYPE) TypeNullability.NONNULL else null
         val annotations = createAnnotations(type.annos())
-        val modifiers = TurbineTypeModifiers(annotations)
+        val modifiers = TurbineTypeModifiers(annotations, nullability)
         val qualifiedName = getQualifiedName(type.sym().binaryName())
-        val parameters = type.targs().map { createType(it, false) }
+        val parameters = type.targs().map { createType(it, false) as TypeArgumentTypeItem }
         return TurbineClassTypeItem(codebase, modifiers, qualifiedName, parameters, outerClass)
     }
 
@@ -569,10 +707,10 @@
     }
 
     private fun createTypeParameter(sym: TyVarSymbol, param: TyVarInfo): TurbineTypeParameterItem {
-        val typeBounds = mutableListOf<TurbineTypeItem>()
+        val typeBounds = mutableListOf<BoundsTypeItem>()
         val upperBounds = param.upperBound()
-        upperBounds.bounds().mapTo(typeBounds) { createType(it, false) }
-        param.lowerBound()?.let { typeBounds.add(createType(it, false)) }
+        upperBounds.bounds().mapTo(typeBounds) { createType(it, false) as BoundsTypeItem }
+        param.lowerBound()?.let { typeBounds.add(createType(it, false) as BoundsTypeItem) }
         val modifiers =
             TurbineModifierItem.create(codebase, 0, createAnnotations(param.annotations()), false)
         val typeParamItem =
@@ -658,7 +796,8 @@
                     methodItem
                 }
         // Ignore default enum methods
-        classItem.methods = methodItems.filter { !isDefaultEnumMethod(classItem, it) }
+        classItem.methods =
+            methodItems.filter { !isDefaultEnumMethod(classItem, it) }.toMutableList()
     }
 
     private fun createParameters(methodItem: TurbineMethodItem, parameters: List<ParamInfo>) {
@@ -731,7 +870,7 @@
     private fun fixCtorReturnType(classItem: TurbineClassItem) {
         val result =
             classItem.constructors.map {
-                it.setReturnType(classItem.toType())
+                it.setReturnType(classItem.type())
                 it
             }
         classItem.constructors = result
diff --git a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineConstructorItem.kt b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineConstructorItem.kt
index a5317d9..3ea4592 100644
--- a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineConstructorItem.kt
+++ b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineConstructorItem.kt
@@ -21,7 +21,7 @@
 import com.android.tools.metalava.model.TypeParameterList
 import com.google.turbine.binder.sym.MethodSymbol
 
-class TurbineConstructorItem(
+internal class TurbineConstructorItem(
     codebase: TurbineBasedCodebase,
     private val name: String,
     methodSymbol: MethodSymbol,
@@ -70,7 +70,7 @@
                     name,
                     symbol,
                     containingClass,
-                    containingClass.toType(),
+                    containingClass.type(),
                     modifiers,
                     parameters,
                     "",
diff --git a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineEnvironmentManager.kt b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineEnvironmentManager.kt
index b3c2fe3..b4939c1 100644
--- a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineEnvironmentManager.kt
+++ b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineEnvironmentManager.kt
@@ -23,7 +23,7 @@
 import java.io.File
 
 /** Manages the objects created when processing sources. */
-class TurbineEnvironmentManager() : EnvironmentManager {
+internal class TurbineEnvironmentManager() : EnvironmentManager {
 
     override fun createSourceParser(
         reporter: Reporter,
diff --git a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineFieldItem.kt b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineFieldItem.kt
index e959f95..643111c 100644
--- a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineFieldItem.kt
+++ b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineFieldItem.kt
@@ -17,15 +17,16 @@
 package com.android.tools.metalava.model.turbine
 
 import com.android.tools.metalava.model.ClassItem
+import com.android.tools.metalava.model.DefaultModifierList
 import com.android.tools.metalava.model.FieldItem
 import com.android.tools.metalava.model.TypeItem
 
-class TurbineFieldItem(
+internal class TurbineFieldItem(
     codebase: TurbineBasedCodebase,
     private val name: String,
-    private val containingClass: TurbineClassItem,
+    private val containingClass: ClassItem,
     private val type: TurbineTypeItem,
-    modifiers: TurbineModifierItem,
+    modifiers: DefaultModifierList,
     documentation: String,
 ) : TurbineItem(codebase, modifiers, documentation), FieldItem {
 
@@ -59,7 +60,32 @@
     override fun type(): TypeItem = type
 
     override fun duplicate(targetContainingClass: ClassItem): FieldItem {
-        TODO("b/295800205")
+        val duplicateField =
+            TurbineFieldItem(
+                codebase,
+                name,
+                targetContainingClass,
+                type.duplicate() as TurbineTypeItem,
+                modifiers.duplicate(),
+                documentation
+            )
+        duplicateField.initialValueWithRequiredConstant = initialValueWithRequiredConstant
+        duplicateField.initialValueWithoutRequiredConstant = initialValueWithoutRequiredConstant
+        duplicateField.modifiers.setOwner(duplicateField)
+        duplicateField.inheritedFrom = containingClass
+
+        // Preserve flags that may have been inherited (propagated) from surrounding packages
+        if (targetContainingClass.hidden) {
+            duplicateField.hidden = true
+        }
+        if (targetContainingClass.removed) {
+            duplicateField.removed = true
+        }
+        if (targetContainingClass.docOnly) {
+            duplicateField.docOnly = true
+        }
+
+        return duplicateField
     }
 
     override fun initialValue(requireConstant: Boolean): Any? {
diff --git a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineItem.kt b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineItem.kt
index a846a46..c3ababb 100644
--- a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineItem.kt
+++ b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineItem.kt
@@ -17,12 +17,13 @@
 package com.android.tools.metalava.model.turbine
 
 import com.android.tools.metalava.model.DefaultItem
+import com.android.tools.metalava.model.DefaultModifierList
 import com.android.tools.metalava.model.MutableModifierList
 import com.android.tools.metalava.model.source.utils.LazyDelegate
 
-abstract class TurbineItem(
+internal abstract class TurbineItem(
     override val codebase: TurbineBasedCodebase,
-    override val modifiers: TurbineModifierItem,
+    override val modifiers: DefaultModifierList,
     override var documentation: String,
 ) : DefaultItem(modifiers) {
 
diff --git a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineMethodItem.kt b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineMethodItem.kt
index 18f6e52..d0b76c8 100644
--- a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineMethodItem.kt
+++ b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineMethodItem.kt
@@ -17,26 +17,28 @@
 package com.android.tools.metalava.model.turbine
 
 import com.android.tools.metalava.model.ClassItem
+import com.android.tools.metalava.model.DefaultModifierList
 import com.android.tools.metalava.model.MethodItem
 import com.android.tools.metalava.model.ParameterItem
+import com.android.tools.metalava.model.ThrowableType
 import com.android.tools.metalava.model.TypeItem
 import com.android.tools.metalava.model.TypeParameterList
 import com.android.tools.metalava.model.computeSuperMethods
 import com.google.turbine.binder.sym.MethodSymbol
 
-open class TurbineMethodItem(
+internal open class TurbineMethodItem(
     codebase: TurbineBasedCodebase,
     private val methodSymbol: MethodSymbol,
-    private val containingClass: TurbineClassItem,
+    private val containingClass: ClassItem,
     protected var returnType: TurbineTypeItem,
-    modifiers: TurbineModifierItem,
+    modifiers: DefaultModifierList,
     private val typeParameters: TypeParameterList,
     documentation: String,
 ) : TurbineItem(codebase, modifiers, documentation), MethodItem {
 
     private lateinit var superMethodList: List<MethodItem>
     internal lateinit var throwsClassNames: List<String>
-    private lateinit var throwsTypes: List<ClassItem>
+    private lateinit var throwsTypes: List<ThrowableType>
     internal lateinit var parameters: List<ParameterItem>
 
     override var inheritedFrom: ClassItem? = null
@@ -47,11 +49,9 @@
 
     override fun returnType(): TypeItem = returnType
 
-    override fun throwsTypes(): List<ClassItem> = throwsTypes
+    override fun throwsTypes(): List<ThrowableType> = throwsTypes
 
-    override fun isExtensionMethod(): Boolean {
-        TODO("b/295800205")
-    }
+    override fun isExtensionMethod(): Boolean = false // java does not support extension methods
 
     override fun isConstructor(): Boolean = false
 
@@ -94,15 +94,56 @@
     @Deprecated("This property should not be accessed directly.")
     override var _requiresOverride: Boolean? = null
 
-    override fun duplicate(targetContainingClass: ClassItem): TurbineMethodItem =
-        TODO("b/295800205")
+    override fun duplicate(targetContainingClass: ClassItem): TurbineMethodItem {
+        // Duplicate the parameters
+        val params = parameters.map { TurbineParameterItem.duplicate(codebase, it, emptyMap()) }
+        val retType = returnType.duplicate()
+        val mods = modifiers.duplicate()
+        val duplicateMethod =
+            TurbineMethodItem(
+                codebase,
+                methodSymbol,
+                targetContainingClass,
+                retType as TurbineTypeItem,
+                mods,
+                typeParameters,
+                documentation
+            )
+        mods.setOwner(duplicateMethod)
+        duplicateMethod.parameters = params
+        duplicateMethod.inheritedFrom = containingClass
+        duplicateMethod.throwsTypes = throwsTypes
+
+        // Preserve flags that may have been inherited (propagated) from surrounding packages
+        if (targetContainingClass.hidden) {
+            duplicateMethod.hidden = true
+        }
+        if (targetContainingClass.removed) {
+            duplicateMethod.removed = true
+        }
+        if (targetContainingClass.docOnly) {
+            duplicateMethod.docOnly = true
+        }
+        if (targetContainingClass.deprecated) {
+            duplicateMethod.deprecated = true
+        }
+
+        return duplicateMethod
+    }
 
     override fun findMainDocumentation(): String = TODO("b/295800205")
 
     override fun typeParameterList(): TypeParameterList = typeParameters
 
     internal fun setThrowsTypes() {
-        val result = throwsClassNames.map { codebase.findOrCreateClass(it)!! }
-        throwsTypes = result.sortedWith(ClassItem.fullNameComparator)
+        val result =
+            throwsClassNames.map { ThrowableType.ofClass(codebase.findOrCreateClass(it)!!) }
+        throwsTypes = result.sortedWith(ThrowableType.fullNameComparator)
     }
+
+    internal fun setThrowsTypes(throwsList: List<ThrowableType>) {
+        throwsTypes = throwsList
+    }
+
+    internal fun getSymbol(): MethodSymbol = methodSymbol
 }
diff --git a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineModifierItem.kt b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineModifierItem.kt
index e78dc98..c04ce9c 100644
--- a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineModifierItem.kt
+++ b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineModifierItem.kt
@@ -22,8 +22,7 @@
 import com.android.tools.metalava.model.MutableModifierList
 import com.google.turbine.model.TurbineFlag
 
-class TurbineModifierItem
-internal constructor(
+internal class TurbineModifierItem(
     codebase: Codebase,
     flags: Int = PACKAGE_PRIVATE,
     annotations: List<AnnotationItem>?
diff --git a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbinePackageItem.kt b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbinePackageItem.kt
index d85034a..1095d82 100644
--- a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbinePackageItem.kt
+++ b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbinePackageItem.kt
@@ -20,7 +20,7 @@
 import com.android.tools.metalava.model.PackageItem
 import com.android.tools.metalava.model.VisibilityLevel
 
-class TurbinePackageItem(
+internal class TurbinePackageItem(
     codebase: TurbineBasedCodebase,
     private val qualifiedName: String,
     modifiers: TurbineModifierItem,
diff --git a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineParameterItem.kt b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineParameterItem.kt
index db5cedb1..834caff 100644
--- a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineParameterItem.kt
+++ b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineParameterItem.kt
@@ -17,19 +17,21 @@
 package com.android.tools.metalava.model.turbine
 
 import com.android.tools.metalava.model.AnnotationItem
+import com.android.tools.metalava.model.DefaultModifierList
 import com.android.tools.metalava.model.MethodItem
 import com.android.tools.metalava.model.ParameterItem
 import com.android.tools.metalava.model.TypeItem
+import com.android.tools.metalava.model.TypeParameterBindings
 import com.android.tools.metalava.model.findAnnotation
 import com.android.tools.metalava.model.hasAnnotation
 
-class TurbineParameterItem(
+internal class TurbineParameterItem(
     codebase: TurbineBasedCodebase,
     private val name: String,
-    private val containingMethod: TurbineMethodItem,
+    private val containingMethod: MethodItem,
     override val parameterIndex: Int,
-    private val type: TurbineTypeItem,
-    modifiers: TurbineModifierItem,
+    private val type: TypeItem,
+    modifiers: DefaultModifierList,
 ) : TurbineItem(codebase, modifiers, ""), ParameterItem {
 
     override fun name(): String = name
@@ -60,4 +62,23 @@
     override fun type(): TypeItem = type
 
     override fun isVarArgs(): Boolean = modifiers.isVarArg()
+
+    companion object {
+        internal fun duplicate(
+            codebase: TurbineBasedCodebase,
+            parameter: ParameterItem,
+            typeParameterBindings: TypeParameterBindings,
+        ): TurbineParameterItem {
+            val type = parameter.type().convertType(typeParameterBindings)
+            val mods = (parameter.modifiers as DefaultModifierList).duplicate()
+            return TurbineParameterItem(
+                codebase,
+                parameter.name(),
+                parameter.containingMethod(),
+                parameter.parameterIndex,
+                type,
+                mods
+            )
+        }
+    }
 }
diff --git a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineSourceParser.kt b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineSourceParser.kt
index 60f0e51..2a74146 100644
--- a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineSourceParser.kt
+++ b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineSourceParser.kt
@@ -20,6 +20,7 @@
 import com.android.tools.metalava.model.ClassResolver
 import com.android.tools.metalava.model.source.SourceCodebase
 import com.android.tools.metalava.model.source.SourceParser
+import com.android.tools.metalava.model.source.SourceSet
 import com.google.turbine.diag.SourceFile
 import com.google.turbine.parse.Parser
 import java.io.File
@@ -35,23 +36,23 @@
      * Returns a codebase initialized from the given Java source files, with the given description.
      */
     override fun parseSources(
-        sources: List<File>,
+        sourceSet: SourceSet,
+        commonSourceSet: SourceSet,
         description: String,
-        sourcePath: List<File>,
         classPath: List<File>,
     ): TurbineBasedCodebase {
-        val rootDir = sourcePath.firstOrNull() ?: File("").canonicalFile
+        val rootDir = sourceSet.sourcePath.firstOrNull() ?: File("").canonicalFile
         val codebase = TurbineBasedCodebase(rootDir, description, annotationManager)
 
-        val sourcefiles = getSourceFiles(sources)
-        val units = sourcefiles.map { it -> Parser.parse(it) }
+        val sourceFiles = getSourceFiles(sourceSet.sources)
+        val units = sourceFiles.map { Parser.parse(it) }
         codebase.initialize(units, classPath)
 
         return codebase
     }
 
     private fun getSourceFiles(sources: List<File>): List<SourceFile> {
-        return sources.map { it -> SourceFile(it.path, it.readText()) }
+        return sources.map { SourceFile(it.path, it.readText()) }
     }
 
     override fun loadFromJar(apiJar: File, preFiltered: Boolean): SourceCodebase {
diff --git a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineTypeItem.kt b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineTypeItem.kt
index 10e34c3..f775eed 100644
--- a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineTypeItem.kt
+++ b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineTypeItem.kt
@@ -21,44 +21,31 @@
 import com.android.tools.metalava.model.DefaultTypeItem
 import com.android.tools.metalava.model.PrimitiveTypeItem
 import com.android.tools.metalava.model.PrimitiveTypeItem.Primitive
+import com.android.tools.metalava.model.ReferenceTypeItem
+import com.android.tools.metalava.model.TypeArgumentTypeItem
 import com.android.tools.metalava.model.TypeItem
-import com.android.tools.metalava.model.TypeModifiers
 import com.android.tools.metalava.model.TypeParameterItem
 import com.android.tools.metalava.model.VariableTypeItem
 import com.android.tools.metalava.model.WildcardTypeItem
 import com.google.turbine.binder.sym.TyVarSymbol
 
-sealed class TurbineTypeItem(
-    open val codebase: TurbineBasedCodebase,
-    override val modifiers: TypeModifiers,
-) : DefaultTypeItem(codebase) {
-
-    override fun asClass(): TurbineClassItem? {
-        if (this is TurbineArrayTypeItem) {
-            return this.componentType.asClass()
-        }
-        if (this is TurbineClassTypeItem) {
-            return codebase.findOrCreateClass(this.qualifiedName)
-        }
-        if (this is TurbineVariableTypeItem) {
-            return codebase.findOrCreateClass(this.toErasedTypeString())
-        }
-        return null
-    }
-}
+internal sealed class TurbineTypeItem(
+    val codebase: TurbineBasedCodebase,
+    override val modifiers: TurbineTypeModifiers,
+) : DefaultTypeItem(codebase) {}
 
 internal class TurbinePrimitiveTypeItem(
-    override val codebase: TurbineBasedCodebase,
-    override val modifiers: TurbineTypeModifiers,
+    codebase: TurbineBasedCodebase,
+    modifiers: TurbineTypeModifiers,
     override val kind: Primitive,
 ) : PrimitiveTypeItem, TurbineTypeItem(codebase, modifiers) {
-    override fun duplicate(): TypeItem =
+    override fun duplicate(): PrimitiveTypeItem =
         TurbinePrimitiveTypeItem(codebase, modifiers.duplicate(), kind)
 }
 
 internal class TurbineArrayTypeItem(
-    override val codebase: TurbineBasedCodebase,
-    override val modifiers: TurbineTypeModifiers,
+    codebase: TurbineBasedCodebase,
+    modifiers: TurbineTypeModifiers,
     override val componentType: TurbineTypeItem,
     override val isVarargs: Boolean,
 ) : ArrayTypeItem, TurbineTypeItem(codebase, modifiers) {
@@ -73,52 +60,60 @@
 }
 
 internal class TurbineClassTypeItem(
-    override val codebase: TurbineBasedCodebase,
-    override val modifiers: TurbineTypeModifiers,
+    codebase: TurbineBasedCodebase,
+    modifiers: TurbineTypeModifiers,
     override val qualifiedName: String,
-    override val parameters: List<TurbineTypeItem>,
+    override val arguments: List<TypeArgumentTypeItem>,
     override val outerClassType: TurbineClassTypeItem?,
 ) : ClassTypeItem, TurbineTypeItem(codebase, modifiers) {
     override val className: String = ClassTypeItem.computeClassName(qualifiedName)
 
-    override fun duplicate(outerClass: ClassTypeItem?, parameters: List<TypeItem>): ClassTypeItem {
+    private val asClassCache by
+        lazy(LazyThreadSafetyMode.NONE) { codebase.resolveClass(qualifiedName) }
+
+    override fun asClass() = asClassCache
+
+    override fun duplicate(
+        outerClass: ClassTypeItem?,
+        arguments: List<TypeArgumentTypeItem>
+    ): ClassTypeItem {
         return TurbineClassTypeItem(
             codebase,
             modifiers.duplicate(),
             qualifiedName,
-            parameters.map { it as TurbineTypeItem },
+            arguments,
             outerClass as? TurbineClassTypeItem
         )
     }
 }
 
 internal class TurbineVariableTypeItem(
-    override val codebase: TurbineBasedCodebase,
-    override val modifiers: TurbineTypeModifiers,
+    codebase: TurbineBasedCodebase,
+    modifiers: TurbineTypeModifiers,
     private val symbol: TyVarSymbol
 ) : VariableTypeItem, TurbineTypeItem(codebase, modifiers) {
     override val name: String = symbol.name()
     override val asTypeParameter: TypeParameterItem by lazy { codebase.findTypeParameter(symbol) }
 
-    override fun duplicate(): TypeItem =
+    override fun duplicate(): VariableTypeItem =
         TurbineVariableTypeItem(codebase, modifiers.duplicate(), symbol)
 }
 
 internal class TurbineWildcardTypeItem(
-    override val codebase: TurbineBasedCodebase,
-    override val modifiers: TurbineTypeModifiers,
-    override val extendsBound: TurbineTypeItem?,
-    override val superBound: TurbineTypeItem?,
+    codebase: TurbineBasedCodebase,
+    modifiers: TurbineTypeModifiers,
+    override val extendsBound: ReferenceTypeItem?,
+    override val superBound: ReferenceTypeItem?,
 ) : WildcardTypeItem, TurbineTypeItem(codebase, modifiers) {
     override fun duplicate(
-        extendsBound: TypeItem?,
-        superBound: TypeItem?
+        extendsBound: ReferenceTypeItem?,
+        superBound: ReferenceTypeItem?
     ): TurbineWildcardTypeItem {
         return TurbineWildcardTypeItem(
             codebase,
             modifiers.duplicate(),
-            extendsBound as? TurbineTypeItem,
-            superBound as? TurbineTypeItem
+            extendsBound,
+            superBound,
         )
     }
 }
diff --git a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineTypeParameterItem.kt b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineTypeParameterItem.kt
index 559f57f..ec8896b 100644
--- a/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineTypeParameterItem.kt
+++ b/metalava-model-turbine/src/main/java/com/android/tools/metalava/model/turbine/TurbineTypeParameterItem.kt
@@ -16,40 +16,44 @@
 
 package com.android.tools.metalava.model.turbine
 
-import com.android.tools.metalava.model.TypeItem
+import com.android.tools.metalava.model.BoundsTypeItem
 import com.android.tools.metalava.model.TypeParameterItem
-import com.android.tools.metalava.model.TypeParameterList
-import com.google.turbine.binder.sym.ClassSymbol
+import com.android.tools.metalava.model.VariableTypeItem
 import com.google.turbine.binder.sym.TyVarSymbol
 
 internal class TurbineTypeParameterItem(
     codebase: TurbineBasedCodebase,
     modifiers: TurbineModifierItem,
     internal val symbol: TyVarSymbol,
-    name: String = symbol.name(),
-    private val bounds: List<TypeItem>,
-    private val document: String = "",
+    private val name: String = symbol.name(),
+    private val bounds: List<BoundsTypeItem>,
 ) :
-    TurbineClassItem(
+    TurbineItem(
         codebase,
-        name,
-        name,
-        name,
-        ClassSymbol(name),
         modifiers,
-        TurbineClassType.TYPE_PARAMETER,
-        TypeParameterList.NONE,
-        document,
-        null,
+        "",
     ),
     TypeParameterItem {
 
+    override fun name() = name
+
     // Java does not supports reified generics
     override fun isReified(): Boolean = false
 
-    override fun typeBounds(): List<TypeItem> = bounds
+    override fun typeBounds(): List<BoundsTypeItem> = bounds
 
-    override fun toType(): TurbineTypeItem {
+    override fun type(): VariableTypeItem {
         return TurbineVariableTypeItem(codebase, TurbineTypeModifiers(emptyList()), symbol)
     }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is TypeParameterItem) return false
+
+        return name == other.name()
+    }
+
+    override fun hashCode(): Int {
+        return name.hashCode()
+    }
 }
diff --git a/metalava-model-turbine/src/test/resources/model-test-suite-baseline.txt b/metalava-model-turbine/src/test/resources/model-test-suite-baseline.txt
index 043dd64..247a049 100644
--- a/metalava-model-turbine/src/test/resources/model-test-suite-baseline.txt
+++ b/metalava-model-turbine/src/test/resources/model-test-suite-baseline.txt
@@ -1,34 +1,28 @@
 com.android.tools.metalava.model.testsuite.annotationitem.CommonAnnotationItemTest
-  annotation array values with single element[turbine,java]
-  annotation toSource() for array values with single element[turbine,java]
-  annotation toSource() with class values[turbine,java]
   annotation toSource() with compound expression values[turbine,java]
-  annotation toSource() with enum values[turbine,java]
-  annotation toSource() with number values[turbine,java]
-  annotation with enum values[turbine,java]
-  annotation with infinity values[turbine,java]
-  annotation with negative values[turbine,java]
-  annotation with type cast values[turbine,java]
-
-com.android.tools.metalava.model.testsuite.classitem.CommonClassItemTest
-  Test inheritMethodFromNonApiAncestor[turbine,java]
+  annotation toSource() with constant literal values[turbine,java]
 
 com.android.tools.metalava.model.testsuite.fielditem.SourceFieldItemTest
   test default value of an enum constant field[turbine,java]
   test non final field with default value as constant expression[turbine,java]
 
+com.android.tools.metalava.model.testsuite.methoditem.CommonMethodItemTest
+  Test throws method type parameter does not extend Throwable[turbine,java]
+  Test throws method type parameter extends Throwable[turbine,java]
+
+com.android.tools.metalava.model.testsuite.methoditem.CommonParameterItemTest
+  Test publicName reports correct name when called on binary class - Object#equals[turbine,java]
+  Test publicName reports correct name when called on binary class - ViewGroup#onLayout[turbine,java]
+
 com.android.tools.metalava.model.testsuite.packageitem.CommonPackageItemTest
   Test @hide in package html[turbine,java]
   Test nullability annotation in package info[turbine,java]
 
-com.android.tools.metalava.model.testsuite.typeitem.CommonInternalNameTest
-  test[turbine,java,pkg.UnknownClass.Inner]
-  test[turbine,java,pkg.UnknownClass]
-
 com.android.tools.metalava.model.testsuite.typeitem.CommonTypeItemTest
   Test inner types from classpath[turbine,java]
 
 com.android.tools.metalava.model.testsuite.typeitem.CommonTypeModifiersTest
   Test interface types[turbine,java]
   Test leading annotation on array type[turbine,java]
+  Test nullability of super interface type[turbine,java]
   Test super class and interface types of interface[turbine,java]
diff --git a/metalava-model/build.gradle.kts b/metalava-model/build.gradle.kts
index b659529..1b29ba2 100644
--- a/metalava-model/build.gradle.kts
+++ b/metalava-model/build.gradle.kts
@@ -27,5 +27,6 @@
     testImplementation(libs.truth)
     testImplementation(libs.kotlinTest)
 
+    testFixturesImplementation(libs.truth)
     testFixturesImplementation(libs.kotlinTest)
 }
diff --git a/metalava-model/src/main/java/com/android/tools/metalava/model/ClassItem.kt b/metalava-model/src/main/java/com/android/tools/metalava/model/ClassItem.kt
index 53789ff..2417661 100644
--- a/metalava-model/src/main/java/com/android/tools/metalava/model/ClassItem.kt
+++ b/metalava-model/src/main/java/com/android/tools/metalava/model/ClassItem.kt
@@ -27,7 +27,7 @@
  * com.android.tools.metalava.model.TypeItem} instead
  */
 @MetalavaApi
-interface ClassItem : Item {
+interface ClassItem : Item, TypeParameterListOwner {
     /** The simple name of a class. In class foo.bar.Outer.Inner, the simple name is "Inner" */
     fun simpleName(): String
 
@@ -99,10 +99,7 @@
 
     /** All super classes, if any */
     fun allSuperClasses(): Sequence<ClassItem> {
-        return superClass()?.let { cls ->
-            return generateSequence(cls) { it.superClass() }
-        }
-            ?: return emptySequence()
+        return generateSequence(superClass()) { it.superClass() }
     }
 
     /**
@@ -111,7 +108,7 @@
      * List<String>" the super class is java.util.List and the super class type is
      * java.util.List<java.lang.String>.
      */
-    fun superClassType(): TypeItem?
+    fun superClassType(): ClassTypeItem?
 
     /** Returns true if this class extends the given class (includes self) */
     fun extends(qualifiedName: String): Boolean {
@@ -154,7 +151,7 @@
         extends(qualifiedName) || implements(qualifiedName)
 
     /** Any interfaces implemented by this class */
-    @MetalavaApi fun interfaceTypes(): List<TypeItem>
+    @MetalavaApi fun interfaceTypes(): List<ClassTypeItem>
 
     /**
      * All classes and interfaces implemented (by this class and its super classes and the
@@ -185,17 +182,19 @@
         return fields().asSequence().plus(constructors().asSequence()).plus(methods().asSequence())
     }
 
+    val classKind: ClassKind
+
     /** Whether this class is an interface */
-    fun isInterface(): Boolean
+    fun isInterface() = classKind == ClassKind.INTERFACE
 
     /** Whether this class is an annotation type */
-    fun isAnnotationType(): Boolean
+    fun isAnnotationType() = classKind == ClassKind.ANNOTATION_TYPE
 
     /** Whether this class is an enum */
-    fun isEnum(): Boolean
+    fun isEnum() = classKind == ClassKind.ENUM
 
     /** Whether this class is a regular class (not an interface, not an enum, etc) */
-    fun isClass(): Boolean = !isInterface() && !isAnnotationType() && !isEnum()
+    fun isClass() = classKind == ClassKind.CLASS
 
     /** The containing class, for inner classes */
     @MetalavaApi override fun containingClass(): ClassItem?
@@ -204,21 +203,13 @@
     override fun containingPackage(): PackageItem
 
     /** Gets the type for this class */
-    fun toType(): TypeItem
-
-    override fun type(): TypeItem? = null
+    override fun type(): ClassTypeItem
 
     override fun findCorrespondingItemIn(codebase: Codebase) = codebase.findClass(qualifiedName())
 
     /** Returns true if this class has type parameters */
     fun hasTypeVariables(): Boolean
 
-    /**
-     * Any type parameters for the class, if any, as a source string (with fully qualified class
-     * names)
-     */
-    @MetalavaApi fun typeParameterList(): TypeParameterList
-
     fun isJavaLangObject(): Boolean {
         return qualifiedName() == JAVA_LANG_OBJECT
     }
@@ -226,11 +217,7 @@
     // Mutation APIs: Used to "fix up" the API hierarchy to only expose visible parts of the API.
 
     // This replaces the interface types implemented by this class
-    fun setInterfaceTypes(interfaceTypes: List<TypeItem>)
-
-    // Whether this class is a generic type parameter, such as T, rather than a non-generic type,
-    // like String
-    val isTypeParameter: Boolean
+    fun setInterfaceTypes(interfaceTypes: List<ClassTypeItem>)
 
     var hasPrivateConstructor: Boolean
 
@@ -459,7 +446,7 @@
     }
 
     fun filteredSuperClassType(predicate: Predicate<Item>): TypeItem? {
-        var superClassType: TypeItem? = superClassType() ?: return null
+        var superClassType: ClassTypeItem? = superClassType() ?: return null
         var prev: ClassItem? = null
         while (superClassType != null) {
             val superClass = superClassType.asClass() ?: return null
@@ -705,7 +692,7 @@
      * `interface Root<T>`, this method will return `{T->X}` as the mapping from `C` to `Root`, not
      * `{T->Y}`.
      */
-    fun mapTypeVariables(target: ClassItem): Map<TypeItem, TypeItem> {
+    fun mapTypeVariables(target: ClassItem): TypeParameterBindings {
         // Gather the supertypes to check for [target]. It is only possible for [target] to be found
         // in the class hierarchy through this class's interfaces if [target] is an interface.
         val candidates =
@@ -717,43 +704,53 @@
 
         for (superClassType in candidates.filterNotNull()) {
             superClassType as? ClassTypeItem ?: continue
-            // Convert the type to a class and then back to a type: this will produce a class type
-            // with the type parameters of the declared class, instead of the type variables used in
-            // this class declaration.
-            // E.g. for `class A<X,Y> extends B<X,Y>`, and `class B<M,N>`, the superClassType has
-            // parameters ["X", "Y"] and the declaredClassType has parameters ["M", 'N"].
-            val asClass = superClassType.asClass() ?: continue
-            val declaredClassType = asClass.toType() as? ClassTypeItem ?: continue
+            // Get the class from the class type so that its type parameters can be accessed.
+            val declaringClass = superClassType.asClass() ?: continue
 
-            if (asClass.qualifiedName() == target.qualifiedName()) {
+            if (declaringClass.qualifiedName() == target.qualifiedName()) {
                 // The target has been found, return the map directly.
-                return mapTypeVariables(declaredClassType, superClassType)
+                return mapTypeVariables(declaringClass, superClassType)
             } else {
                 // This superClassType isn't target, but maybe it has target as a superclass.
-                val nextLevelMap = asClass.mapTypeVariables(target)
+                val nextLevelMap = declaringClass.mapTypeVariables(target)
                 if (nextLevelMap.isNotEmpty()) {
-                    val thisLevelMap = mapTypeVariables(declaredClassType, superClassType)
+                    val thisLevelMap = mapTypeVariables(declaringClass, superClassType)
                     // Link the two maps by removing intermediate type variables.
-                    return nextLevelMap.mapValues { (_, value) -> thisLevelMap[value] ?: value }
+                    return nextLevelMap.mapValues { (_, value) ->
+                        (value as? VariableTypeItem?)?.let { thisLevelMap[it.asTypeParameter] }
+                            ?: value
+                    }
                 }
             }
         }
         return emptyMap()
     }
 
-    /** Creates a map between the parameters of [c1] and the parameters of [c2]. */
-    private fun mapTypeVariables(c1: ClassTypeItem, c2: ClassTypeItem): Map<TypeItem, TypeItem> {
-        // Don't include parameters of class types, for consistency with the old psi implementation.
+    /**
+     * Creates a map between the type parameters of [declaringClass] and the arguments of
+     * [classTypeItem].
+     */
+    private fun mapTypeVariables(
+        declaringClass: ClassItem,
+        classTypeItem: ClassTypeItem
+    ): TypeParameterBindings {
+        // Don't include arguments of class types, for consistency with the old psi implementation.
+        // i.e. if the mapping is from `T -> List<String>` then just use `T -> List`.
         // TODO (b/319300404): remove this section
-        val c2Params =
-            c2.parameters.map {
-                if (it is ClassTypeItem && it.parameters.isNotEmpty()) {
-                    it.duplicate(it.outerClassType, parameters = emptyList())
+        val classTypeArguments =
+            classTypeItem.arguments.map {
+                if (it is ClassTypeItem && it.arguments.isNotEmpty()) {
+                    it.duplicate(it.outerClassType, arguments = emptyList())
                 } else {
                     it
                 }
+                // Although a `ClassTypeItem`'s arguments can be `WildcardTypeItem`s as well as
+                // `ReferenceTypeItem`s, a `ClassTypeItem` used in an extends or implements list
+                // cannot have a `WildcardTypeItem` as an argument so this cast is safe. See
+                // https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-Superclass
+                as ReferenceTypeItem
             }
-        return c1.parameters.zip(c2Params).toMap()
+        return declaringClass.typeParameterList().typeParameters().zip(classTypeArguments).toMap()
     }
 
     /** Creates a constructor in this class */
diff --git a/metalava-model/src/main/java/com/android/tools/metalava/model/ClassKind.kt b/metalava-model/src/main/java/com/android/tools/metalava/model/ClassKind.kt
new file mode 100644
index 0000000..db9aaa06
--- /dev/null
+++ b/metalava-model/src/main/java/com/android/tools/metalava/model/ClassKind.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.model
+
+/**
+ * The kind of class.
+ *
+ * Corresponds to similarly named values in [javax.lang.model.element.ElementKind].
+ */
+enum class ClassKind {
+    /** An interface. */
+    INTERFACE,
+
+    /** An enum class. */
+    ENUM,
+
+    /** An annotation class. */
+    ANNOTATION_TYPE,
+
+    /** A normal class. */
+    CLASS
+}
diff --git a/metalava-model/src/main/java/com/android/tools/metalava/model/Codebase.kt b/metalava-model/src/main/java/com/android/tools/metalava/model/Codebase.kt
index 3f2fa26..59f6dbc 100644
--- a/metalava-model/src/main/java/com/android/tools/metalava/model/Codebase.kt
+++ b/metalava-model/src/main/java/com/android/tools/metalava/model/Codebase.kt
@@ -44,6 +44,15 @@
     /** Returns a class identified by fully qualified name, if in the codebase */
     fun findClass(className: String): ClassItem?
 
+    /**
+     * Resolve a class identified by fully qualified name.
+     *
+     * This does everything it can to retrieve a suitable class, e.g. searching classpath (if
+     * available). That may include fabricating the [ClassItem] from nothing in the case of models
+     * that work with a partial set of classes (like text model).
+     */
+    fun resolveClass(className: String): ClassItem?
+
     /** Returns a package identified by fully qualified name, if in the codebase */
     fun findPackage(pkgName: String): PackageItem?
 
diff --git a/metalava-model/src/main/java/com/android/tools/metalava/model/Item.kt b/metalava-model/src/main/java/com/android/tools/metalava/model/Item.kt
index 5fc7a64..08e241c 100644
--- a/metalava-model/src/main/java/com/android/tools/metalava/model/Item.kt
+++ b/metalava-model/src/main/java/com/android/tools/metalava/model/Item.kt
@@ -259,9 +259,15 @@
     fun containingClass(): ClassItem?
 
     /**
-     * Returns the associated type if any. For example, for a field, property or parameter, this is
-     * the type of the variable; for a method, it's the return type. For packages, classes and
-     * files, it's null.
+     * Returns the associated type, if any.
+     *
+     * i.e.
+     * * For a field, property or parameter, this is the type of the variable.
+     * * For a method, it's the return type.
+     * * For classes it's the declared class type, i.e. a class type using the type parameter types
+     *   as the type arguments.
+     * * For type parameters it's a [VariableTypeItem] reference the type parameter.
+     * * For packages and files, it's null.
      */
     fun type(): TypeItem?
 
diff --git a/metalava-model/src/main/java/com/android/tools/metalava/model/MethodItem.kt b/metalava-model/src/main/java/com/android/tools/metalava/model/MethodItem.kt
index ddc1052..9952ccc 100644
--- a/metalava-model/src/main/java/com/android/tools/metalava/model/MethodItem.kt
+++ b/metalava-model/src/main/java/com/android/tools/metalava/model/MethodItem.kt
@@ -19,7 +19,7 @@
 import java.util.function.Predicate
 
 @MetalavaApi
-interface MethodItem : MemberItem {
+interface MethodItem : MemberItem, TypeParameterListOwner {
     /**
      * The property this method is an accessor for; inverse of [PropertyItem.getter] and
      * [PropertyItem.setter]
@@ -42,7 +42,7 @@
     /** Returns the super methods that this method is overriding */
     fun superMethods(): List<MethodItem>
 
-    override fun type(): TypeItem? = returnType()
+    override fun type() = returnType()
 
     override fun findCorrespondingItemIn(codebase: Codebase) =
         containingClass().findCorrespondingItemIn(codebase)?.findMethod(this)
@@ -58,25 +58,14 @@
         }
     }
 
-    /**
-     * Any type parameters for the class, if any, as a source string (with fully qualified class
-     * names)
-     */
-    @MetalavaApi fun typeParameterList(): TypeParameterList
-
     /** Types of exceptions that this method can throw */
-    fun throwsTypes(): List<ClassItem>
+    fun throwsTypes(): List<ThrowableType>
 
-    /** Returns true if this class throws the given exception */
+    /** Returns true if this method throws the given exception */
     fun throws(qualifiedName: String): Boolean {
         for (type in throwsTypes()) {
-            if (type.extends(qualifiedName)) {
-                return true
-            }
-        }
-
-        for (type in throwsTypes()) {
-            if (type.qualifiedName() == qualifiedName) {
+            val throwableClass = type.throwableClass ?: continue
+            if (throwableClass.extends(qualifiedName)) {
                 return true
             }
         }
@@ -84,7 +73,7 @@
         return false
     }
 
-    fun filteredThrowsTypes(predicate: Predicate<Item>): Collection<ClassItem> {
+    fun filteredThrowsTypes(predicate: Predicate<Item>): Collection<ThrowableType> {
         if (throwsTypes().isEmpty()) {
             return emptyList()
         }
@@ -93,25 +82,21 @@
 
     private fun filteredThrowsTypes(
         predicate: Predicate<Item>,
-        classes: LinkedHashSet<ClassItem>
-    ): LinkedHashSet<ClassItem> {
-        for (cls in throwsTypes()) {
-            if (predicate.test(cls) || cls.isTypeParameter) {
-                classes.add(cls)
+        throwableTypes: LinkedHashSet<ThrowableType>
+    ): LinkedHashSet<ThrowableType> {
+        for (throwableType in throwsTypes()) {
+            if (throwableType.isTypeParameter || predicate.test(throwableType.classItem)) {
+                throwableTypes.add(throwableType)
             } else {
                 // Excluded, but it may have super class throwables that are included; if so,
-                // include those
-                var curr = cls.superClass()
-                while (curr != null) {
-                    if (predicate.test(curr)) {
-                        classes.add(curr)
-                        break
-                    }
-                    curr = curr.superClass()
-                }
+                // include those.
+                throwableType.classItem
+                    .allSuperClasses()
+                    .firstOrNull { superClass -> predicate.test(superClass) }
+                    ?.let { superClass -> throwableTypes.add(ThrowableType.ofClass(superClass)) }
             }
         }
-        return classes
+        return throwableTypes
     }
 
     /**
@@ -465,7 +450,7 @@
             is ClassTypeItem ->
                 asClass()?.let { !filterReference.test(it) } == true ||
                     outerClassType?.hasHiddenType(filterReference) == true ||
-                    parameters.any { it.hasHiddenType(filterReference) }
+                    arguments.any { it.hasHiddenType(filterReference) }
             is VariableTypeItem -> !filterReference.test(asTypeParameter)
             is WildcardTypeItem ->
                 extendsBound?.hasHiddenType(filterReference) == true ||
diff --git a/metalava-model/src/main/java/com/android/tools/metalava/model/ThrowableType.kt b/metalava-model/src/main/java/com/android/tools/metalava/model/ThrowableType.kt
new file mode 100644
index 0000000..8d98fcb
--- /dev/null
+++ b/metalava-model/src/main/java/com/android/tools/metalava/model/ThrowableType.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.model
+
+/**
+ * Represents a type which can be used in a `throws` declaration, e.g. a non-generic class, or a
+ * reference to a type parameter that extends [java.lang.Throwable] or one of its subclasses.
+ *
+ * This is currently an alias for [ClassItem] but it will eventually be migrated to a completely
+ * separate type.
+ */
+interface ThrowableType {
+    /** True if [classItem] is a [TypeParameterItem]. */
+    val isTypeParameter: Boolean
+
+    /**
+     * The underlying [ClassItem], if available; must only be called if [isTypeParameter] is
+     * `false`.
+     */
+    val classItem: ClassItem
+
+    /**
+     * The underlying [TypeParameterItem], if available; must only be called if [isTypeParameter] is
+     * `true`.
+     */
+    val typeParameterItem: TypeParameterItem
+
+    /**
+     * The optional [ClassItem] that is a subclass of [java.lang.Throwable].
+     *
+     * When the underlying [classItem] is a [TypeParameterItem] this will return the erased type
+     * class, if available, or `null` otherwise. When the underlying [classItem] is not a
+     * [TypeParameterItem] then this will just return [classItem].
+     */
+    val throwableClass: ClassItem?
+
+    /** A description of the `ThrowableType`, suitable for use in reports. */
+    fun description(): String
+
+    /** The full name of the underlying [classItem]. */
+    fun fullName(): String
+
+    /** The fully qualified name, will be the simple name of a [TypeParameterItem]. */
+    fun qualifiedName(): String
+
+    /** A wrapper of [ClassItem] that implements [ThrowableType]. */
+    private class ThrowableClassItem(override val classItem: ClassItem) : ThrowableType {
+
+        /* This is never a type parameter. */
+        override val isTypeParameter
+            get() = false
+
+        override val typeParameterItem: TypeParameterItem
+            get() = error("Cannot access `typeParameterItem` on $this")
+
+        /** The [classItem] is a subclass of [java.lang.Throwable] */
+        override val throwableClass: ClassItem
+            get() = classItem
+
+        override fun description() = qualifiedName()
+
+        override fun fullName() = classItem.fullName()
+
+        override fun qualifiedName() = classItem.qualifiedName()
+
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (javaClass != other?.javaClass) return false
+
+            other as ThrowableClassItem
+
+            if (classItem != other.classItem) return false
+
+            return true
+        }
+
+        override fun hashCode(): Int {
+            return classItem.hashCode()
+        }
+
+        override fun toString() = classItem.toString()
+    }
+
+    /** A wrapper of [TypeParameterItem] that implements [ThrowableType]. */
+    private class ThrowableTypeParameterItem(override val typeParameterItem: TypeParameterItem) :
+        ThrowableType {
+
+        /** This is always a type parameter. */
+        override val isTypeParameter
+            get() = true
+
+        /** The [typeParameterItem] has no underlying [ClassItem]. */
+        override val classItem: ClassItem
+            get() = error("Cannot access classItem of $this")
+
+        /** The [throwableClass] is the lower bounds of [typeParameterItem]. */
+        override val throwableClass: ClassItem?
+            get() = typeParameterItem.typeBounds().firstNotNullOfOrNull { it.asClass() }
+
+        override fun description() =
+            "${typeParameterItem.name()} (extends ${throwableClass?.qualifiedName() ?: "unknown throwable"})}"
+
+        /** A TypeParameterItem name is not prefixed by a containing class. */
+        override fun fullName() = typeParameterItem.name()
+
+        /** A TypeParameterItem name is not qualified by the package. */
+        override fun qualifiedName() = typeParameterItem.name()
+
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (javaClass != other?.javaClass) return false
+
+            other as ThrowableTypeParameterItem
+
+            if (typeParameterItem != other.typeParameterItem) return false
+
+            return true
+        }
+
+        override fun hashCode(): Int {
+            return typeParameterItem.hashCode()
+        }
+
+        override fun toString() = typeParameterItem.toString()
+    }
+
+    companion object {
+        /** Get a [ThrowableType] wrapper around [ClassItem] */
+        fun ofClass(classItem: ClassItem): ThrowableType =
+            if (classItem is TypeParameterItem) error("Must not call this with a TypeParameterItem")
+            else ThrowableClassItem(classItem)
+
+        /** Get a [ThrowableType] wrapper around [TypeParameterItem] */
+        fun ofTypeParameter(classItem: TypeParameterItem): ThrowableType =
+            ThrowableTypeParameterItem(classItem)
+
+        /** A partial ordering over [ThrowableType] comparing [ThrowableType.fullName]. */
+        val fullNameComparator: Comparator<ThrowableType> = Comparator.comparing { it.fullName() }
+    }
+}
diff --git a/metalava-model/src/main/java/com/android/tools/metalava/model/TypeItem.kt b/metalava-model/src/main/java/com/android/tools/metalava/model/TypeItem.kt
index e880bc4..3b6bde9 100644
--- a/metalava-model/src/main/java/com/android/tools/metalava/model/TypeItem.kt
+++ b/metalava-model/src/main/java/com/android/tools/metalava/model/TypeItem.kt
@@ -124,15 +124,16 @@
     }
 
     /**
-     * Makes substitutions to the type based on the [replacementMap]. For instance, if the
-     * [replacementMap] contains `{T -> String}`, calling this method on `T` would return `String`,
-     * and calling it on `List<T>` would return `List<String>` (in both cases the modifiers on the
-     * `String` will be independently mutable from the `String` in the [replacementMap]). Calling it
-     * on an unrelated type like `int` would return a duplicate of that type.
+     * Makes substitutions to the type based on the [typeParameterBindings]. For instance, if the
+     * [typeParameterBindings] contains `{T -> String}`, calling this method on `T` would return
+     * `String`, and calling it on `List<T>` would return `List<String>` (in both cases the
+     * modifiers on the `String` will be independently mutable from the `String` in the
+     * [typeParameterBindings]). Calling it on an unrelated type like `int` would return a duplicate
+     * of that type.
      *
      * This method is intended to be used in conjunction with [ClassItem.mapTypeVariables],
      */
-    fun convertType(replacementMap: Map<TypeItem, TypeItem>): TypeItem
+    fun convertType(typeParameterBindings: TypeParameterBindings): TypeItem
 
     fun convertType(from: ClassItem, to: ClassItem): TypeItem {
         val map = from.mapTypeVariables(to)
@@ -151,8 +152,6 @@
 
     fun defaultValueString(): String = "null"
 
-    fun hasTypeArguments(): Boolean = toTypeString().contains("<")
-
     /** Creates an identical type, with a copy of this type's modifiers so they can be mutated. */
     fun duplicate(): TypeItem
 
@@ -335,6 +334,20 @@
     }
 }
 
+/**
+ * A mapping from one class's type parameters to the types provided for those type parameters in a
+ * possibly indirect subclass.
+ *
+ * e.g. Given `Map<K, V>` and a subinterface `StringToIntMap extends Map<String, Integer>` then this
+ * would contain a mapping from `K -> String` and `V -> Integer`.
+ *
+ * Although a `ClassTypeItem`'s arguments can be `WildcardTypeItem`s as well as
+ * `ReferenceTypeItem`s, a `ClassTypeItem` used in an extends or implements list cannot have a
+ * `WildcardTypeItem` as an argument so this cast is safe. See
+ * https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-Superclass
+ */
+typealias TypeParameterBindings = Map<TypeParameterItem, ReferenceTypeItem>
+
 abstract class DefaultTypeItem(private val codebase: Codebase) : TypeItem {
 
     private lateinit var cachedDefaultType: String
@@ -487,11 +500,11 @@
                         }
                     }
 
-                    if (type.parameters.isNotEmpty()) {
+                    if (type.arguments.isNotEmpty()) {
                         append("<")
-                        type.parameters.forEachIndexed { index, parameter ->
+                        type.arguments.forEachIndexed { index, parameter ->
                             appendTypeString(parameter, configuration)
-                            if (index != type.parameters.size - 1) {
+                            if (index != type.arguments.size - 1) {
                                 append(",")
                                 if (configuration.spaceBetweenParameters) {
                                     append(" ")
@@ -579,9 +592,7 @@
                 }
                 is ClassTypeItem -> append(type.qualifiedName)
                 is VariableTypeItem ->
-                    type.asTypeParameter.typeBounds().firstOrNull()?.let {
-                        appendErasedTypeString(it)
-                    }
+                    type.asTypeParameter.asErasedType()?.let { appendErasedTypeString(it) }
                         ?: append(JAVA_LANG_OBJECT)
                 else ->
                     throw IllegalStateException(
@@ -652,6 +663,39 @@
     }
 }
 
+/**
+ * The type for [ClassTypeItem.arguments].
+ *
+ * See https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-TypeArgument.
+ */
+interface TypeArgumentTypeItem : TypeItem {
+    /** Override to specialize the return type. */
+    override fun convertType(typeParameterBindings: TypeParameterBindings): TypeArgumentTypeItem
+
+    /** Override to specialize the return type. */
+    override fun duplicate(): TypeArgumentTypeItem
+}
+
+/**
+ * The type for a reference.
+ *
+ * See https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-ReferenceType.
+ */
+interface ReferenceTypeItem : TypeItem, TypeArgumentTypeItem {
+    /** Override to specialize the return type. */
+    override fun convertType(typeParameterBindings: TypeParameterBindings): ReferenceTypeItem
+
+    /** Override to specialize the return type. */
+    override fun duplicate(): ReferenceTypeItem
+}
+
+/**
+ * The type of [TypeParameterItem]'s type bounds.
+ *
+ * See https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-TypeBound
+ */
+interface BoundsTypeItem : TypeItem, ReferenceTypeItem
+
 /** Represents a primitive type, like int or boolean. */
 interface PrimitiveTypeItem : TypeItem {
     /** The kind of [Primitive] this type is. */
@@ -682,8 +726,10 @@
         visitor.visit(this)
     }
 
-    override fun convertType(replacementMap: Map<TypeItem, TypeItem>): TypeItem {
-        return (replacementMap[this] ?: this).duplicate()
+    override fun duplicate(): PrimitiveTypeItem
+
+    override fun convertType(typeParameterBindings: TypeParameterBindings): PrimitiveTypeItem {
+        return duplicate()
     }
 
     override fun equalToType(other: TypeItem?): Boolean {
@@ -691,10 +737,12 @@
     }
 
     override fun hashCodeForType(): Int = kind.hashCode()
+
+    override fun asClass(): ClassItem? = null
 }
 
 /** Represents an array type, including vararg types. */
-interface ArrayTypeItem : TypeItem {
+interface ArrayTypeItem : TypeItem, ReferenceTypeItem {
     /** The array's inner type (which for multidimensional arrays is another array type). */
     val componentType: TypeItem
 
@@ -715,9 +763,8 @@
      */
     fun duplicate(componentType: TypeItem): ArrayTypeItem
 
-    override fun convertType(replacementMap: Map<TypeItem, TypeItem>): TypeItem {
-        return replacementMap[this]?.duplicate()
-            ?: duplicate(componentType.convertType(replacementMap))
+    override fun convertType(typeParameterBindings: TypeParameterBindings): ArrayTypeItem {
+        return duplicate(componentType.convertType(typeParameterBindings))
     }
 
     override fun equalToType(other: TypeItem?): Boolean {
@@ -726,15 +773,22 @@
     }
 
     override fun hashCodeForType(): Int = Objects.hash(isVarargs, componentType)
+
+    override fun asClass(): ClassItem? = componentType.asClass()
 }
 
 /** Represents a class type. */
-interface ClassTypeItem : TypeItem {
+interface ClassTypeItem : TypeItem, BoundsTypeItem, ReferenceTypeItem {
     /** The qualified name of this class, e.g. "java.lang.String". */
     val qualifiedName: String
 
-    /** The class's parameter types, empty if it has none. */
-    val parameters: List<TypeItem>
+    /**
+     * The class type's arguments, empty if it has none.
+     *
+     * i.e. The specific types that this class type assigns to each of the referenced [ClassItem]'s
+     * type parameters.
+     */
+    val arguments: List<TypeArgumentTypeItem>
 
     /** The outer class type of this class, if it is an inner type. */
     val outerClassType: ClassTypeItem?
@@ -749,38 +803,44 @@
         visitor.visit(this)
     }
 
+    /**
+     * Check to see whether this type has any type arguments.
+     *
+     * It will return `true` for say `List<T>`, but `false` for `String`.
+     */
+    fun hasTypeArguments() = arguments.isNotEmpty()
+
     override fun isString(): Boolean = qualifiedName == JAVA_LANG_STRING
 
     override fun isJavaLangObject(): Boolean = qualifiedName == JAVA_LANG_OBJECT
 
     override fun duplicate(): ClassTypeItem =
-        duplicate(outerClassType?.duplicate(), parameters.map { it.duplicate() })
+        duplicate(outerClassType?.duplicate(), arguments.map { it.duplicate() })
 
     /**
-     * Duplicates this type (including duplicating the modifiers so they can be independently
-     * mutated), but substituting in the provided [outerClass] and [parameters] in place of this
-     * type's outer class and parameters.
+     * Duplicates this type (including duplicating the modifiers, so they can be independently
+     * mutated), but substituting in the provided [outerClass] and [arguments] in place of this
+     * instance's [outerClass] and [arguments].
      */
-    fun duplicate(outerClass: ClassTypeItem?, parameters: List<TypeItem>): ClassTypeItem
+    fun duplicate(outerClass: ClassTypeItem?, arguments: List<TypeArgumentTypeItem>): ClassTypeItem
 
-    override fun convertType(replacementMap: Map<TypeItem, TypeItem>): TypeItem {
-        return replacementMap[this]?.duplicate()
-            ?: duplicate(
-                outerClassType?.convertType(replacementMap) as? ClassTypeItem,
-                parameters.map { it.convertType(replacementMap) }
-            )
+    override fun convertType(typeParameterBindings: TypeParameterBindings): ClassTypeItem {
+        return duplicate(
+            outerClassType?.convertType(typeParameterBindings),
+            arguments.map { it.convertType(typeParameterBindings) }
+        )
     }
 
     override fun equalToType(other: TypeItem?): Boolean {
         if (other !is ClassTypeItem) return false
         return qualifiedName == other.qualifiedName &&
-            parameters.size == other.parameters.size &&
-            parameters.zip(other.parameters).all { (p1, p2) -> p1.equalToType(p2) } &&
+            arguments.size == other.arguments.size &&
+            arguments.zip(other.arguments).all { (p1, p2) -> p1.equalToType(p2) } &&
             ((outerClassType == null && other.outerClassType == null) ||
                 outerClassType?.equalToType(other.outerClassType) == true)
     }
 
-    override fun hashCodeForType(): Int = Objects.hash(qualifiedName, outerClassType, parameters)
+    override fun hashCodeForType(): Int = Objects.hash(qualifiedName, outerClassType, arguments)
 
     companion object {
         /** Computes the simple name of a class from a qualified class name. */
@@ -796,7 +856,7 @@
 }
 
 /** Represents a type variable type. */
-interface VariableTypeItem : TypeItem {
+interface VariableTypeItem : TypeItem, BoundsTypeItem, ReferenceTypeItem {
     /** The name of the type variable */
     val name: String
 
@@ -807,10 +867,14 @@
         visitor.visit(this)
     }
 
-    override fun convertType(replacementMap: Map<TypeItem, TypeItem>): TypeItem {
-        return (replacementMap[this] ?: this).duplicate()
+    override fun convertType(typeParameterBindings: TypeParameterBindings): ReferenceTypeItem {
+        return (typeParameterBindings[asTypeParameter] ?: this).duplicate()
     }
 
+    override fun asClass() = asTypeParameter.asErasedType()?.asClass()
+
+    override fun duplicate(): VariableTypeItem
+
     override fun equalToType(other: TypeItem?): Boolean {
         return (other as? VariableTypeItem)?.name == name
     }
@@ -822,12 +886,12 @@
  * Represents a wildcard type, like `?`, `? extends String`, and `? super String` in Java, or `*`,
  * `out String`, and `in String` in Kotlin.
  */
-interface WildcardTypeItem : TypeItem {
+interface WildcardTypeItem : TypeItem, TypeArgumentTypeItem {
     /** The type this wildcard must extend. If null, the extends bound is implicitly `Object`. */
-    val extendsBound: TypeItem?
+    val extendsBound: ReferenceTypeItem?
 
     /** The type this wildcard must be a super class of. */
-    val superBound: TypeItem?
+    val superBound: ReferenceTypeItem?
 
     override fun accept(visitor: TypeVisitor) {
         visitor.visit(this)
@@ -841,14 +905,16 @@
      * mutated), but substituting in the provided [extendsBound] and [superBound] in place of this
      * type's bounds.
      */
-    fun duplicate(extendsBound: TypeItem?, superBound: TypeItem?): WildcardTypeItem
+    fun duplicate(
+        extendsBound: ReferenceTypeItem?,
+        superBound: ReferenceTypeItem?,
+    ): WildcardTypeItem
 
-    override fun convertType(replacementMap: Map<TypeItem, TypeItem>): TypeItem {
-        return replacementMap[this]?.duplicate()
-            ?: duplicate(
-                extendsBound?.convertType(replacementMap),
-                superBound?.convertType(replacementMap)
-            )
+    override fun convertType(typeParameterBindings: TypeParameterBindings): WildcardTypeItem {
+        return duplicate(
+            extendsBound?.convertType(typeParameterBindings),
+            superBound?.convertType(typeParameterBindings)
+        )
     }
 
     override fun equalToType(other: TypeItem?): Boolean {
@@ -858,4 +924,19 @@
     }
 
     override fun hashCodeForType(): Int = Objects.hash(extendsBound, superBound)
+
+    override fun asClass(): ClassItem? {
+        TODO("Not yet implemented")
+    }
+}
+
+/** Different uses to which a [TypeItem] might be used that might affect its construction. */
+enum class TypeUse {
+    /** General type use; no special behavior. */
+    GENERAL,
+
+    /**
+     * Super type, e.g. in an `extends` or `implements` list; is always [TypeNullability.NONNULL].
+     */
+    SUPER_TYPE,
 }
diff --git a/metalava-model/src/main/java/com/android/tools/metalava/model/TypeParameterItem.kt b/metalava-model/src/main/java/com/android/tools/metalava/model/TypeParameterItem.kt
index 4d5e158..677aa2d 100644
--- a/metalava-model/src/main/java/com/android/tools/metalava/model/TypeParameterItem.kt
+++ b/metalava-model/src/main/java/com/android/tools/metalava/model/TypeParameterItem.kt
@@ -17,7 +17,14 @@
 package com.android.tools.metalava.model
 
 @MetalavaApi
-interface TypeParameterItem : ClassItem {
+interface TypeParameterItem : Item {
+
+    /** The name of the type parameter. */
+    fun name(): String
+
+    /** The [VariableTypeItem] representing the type of this type parameter. */
+    override fun type(): VariableTypeItem
+
     @Deprecated(
         message = "Please use typeBounds() instead.",
         level = DeprecationLevel.ERROR,
@@ -26,7 +33,15 @@
     @MetalavaApi
     fun bounds(): List<ClassItem> = typeBounds().mapNotNull { it.asClass() }
 
-    fun typeBounds(): List<TypeItem>
+    fun typeBounds(): List<BoundsTypeItem>
+
+    /**
+     * Get the erased type of this, i.e. the type that would be used at runtime to represent
+     * something of this type. That is either the first bound (the super class) or
+     * `java.lang.Object` if there are no bounds.
+     */
+    fun asErasedType(): BoundsTypeItem? =
+        typeBounds().firstOrNull() ?: codebase.resolveClass(JAVA_LANG_OBJECT)?.type()
 
     fun isReified(): Boolean
 
@@ -35,7 +50,7 @@
             if (isReified()) {
                 append("reified ")
             }
-            append(simpleName())
+            append(name())
             // If the only bound is Object, omit it because it is implied.
             if (
                 typeBounds().isNotEmpty() && typeBounds().singleOrNull()?.isJavaLangObject() != true
@@ -54,4 +69,16 @@
             }
         }
     }
+
+    // Methods from [Item] that are not needed. They will be removed in a follow-up change.
+    override fun parent() = error("Not needed for TypeParameterItem")
+
+    override fun accept(visitor: ItemVisitor) = error("Not needed for TypeParameterItem")
+
+    override fun containingPackage() = error("Not needed for TypeParameterItem")
+
+    override fun containingClass() = error("Not needed for TypeParameterItem")
+
+    override fun findCorrespondingItemIn(codebase: Codebase) =
+        error("Not needed for TypeParameterItem")
 }
diff --git a/metalava-model/src/main/java/com/android/tools/metalava/model/TypeParameterListOwner.kt b/metalava-model/src/main/java/com/android/tools/metalava/model/TypeParameterListOwner.kt
index 1f7bf7a..3b28660 100644
--- a/metalava-model/src/main/java/com/android/tools/metalava/model/TypeParameterListOwner.kt
+++ b/metalava-model/src/main/java/com/android/tools/metalava/model/TypeParameterListOwner.kt
@@ -16,11 +16,10 @@
 
 package com.android.tools.metalava.model
 
-interface TypeParameterListOwner {
-    fun typeParameterList(): TypeParameterList
-    /** Given a variable in this owner, resolves to a type parameter item */
-    fun resolveParameter(variable: String): TypeParameterItem?
-
-    /** Parent type parameter list owner */
-    fun typeParameterListOwnerParent(): TypeParameterListOwner?
+/** Interface common to all [Item]s that can have type parameters. */
+sealed interface TypeParameterListOwner {
+    /**
+     * Any type parameters for the [Item], if there are no parameters then [TypeParameterList.NONE].
+     */
+    @MetalavaApi fun typeParameterList(): TypeParameterList
 }
diff --git a/metalava-model/src/main/java/com/android/tools/metalava/model/TypeVisitor.kt b/metalava-model/src/main/java/com/android/tools/metalava/model/TypeVisitor.kt
index 534a05e..39300ae 100644
--- a/metalava-model/src/main/java/com/android/tools/metalava/model/TypeVisitor.kt
+++ b/metalava-model/src/main/java/com/android/tools/metalava/model/TypeVisitor.kt
@@ -46,7 +46,7 @@
         visitClassType(classType)
 
         classType.outerClassType?.accept(this)
-        classType.parameters.forEach { it.accept(this) }
+        classType.arguments.forEach { it.accept(this) }
     }
 
     override fun visit(variableType: VariableTypeItem) {
diff --git a/metalava-model/src/test/java/com/android/tools/metalava/model/DefaultAnnotationItemTest.kt b/metalava-model/src/test/java/com/android/tools/metalava/model/DefaultAnnotationItemTest.kt
index 026c3af..599489a 100644
--- a/metalava-model/src/test/java/com/android/tools/metalava/model/DefaultAnnotationItemTest.kt
+++ b/metalava-model/src/test/java/com/android/tools/metalava/model/DefaultAnnotationItemTest.kt
@@ -26,17 +26,19 @@
     // Placeholder for use in test where we don't need codebase functionality
     private val placeholderCodebase =
         object : DefaultCodebase(File("").canonicalFile, "", false, noOpAnnotationManager) {
-            override fun supportsDocumentation(): Boolean = false
+            override fun supportsDocumentation() = false
 
-            override fun getPackages(): PackageList = unsupported()
+            override fun getPackages() = unsupported()
 
-            override fun size(): Int = unsupported()
+            override fun size() = unsupported()
 
-            override fun findClass(className: String): ClassItem? = unsupported()
+            override fun findClass(className: String) = unsupported()
 
-            override fun findPackage(pkgName: String): PackageItem? = unsupported()
+            override fun resolveClass(className: String) = unsupported()
 
-            override fun trustedApi(): Boolean = false
+            override fun findPackage(pkgName: String) = unsupported()
+
+            override fun trustedApi() = false
 
             override fun createAnnotation(
                 source: String,
diff --git a/metalava-model/src/testFixtures/java/com/android/tools/metalava/model/Assertions.kt b/metalava-model/src/testFixtures/java/com/android/tools/metalava/model/Assertions.kt
index 7f28764..ebbc5e5 100644
--- a/metalava-model/src/testFixtures/java/com/android/tools/metalava/model/Assertions.kt
+++ b/metalava-model/src/testFixtures/java/com/android/tools/metalava/model/Assertions.kt
@@ -16,6 +16,7 @@
 
 package com.android.tools.metalava.model
 
+import com.google.common.truth.Truth.assertThat
 import kotlin.test.assertIs
 import kotlin.test.assertNotNull
 
@@ -24,43 +25,59 @@
     /** Get the class from the [Codebase], failing if it does not exist. */
     fun Codebase.assertClass(qualifiedName: String): ClassItem {
         val classItem = findClass(qualifiedName)
-        assertNotNull(classItem) { "Expected $qualifiedName to be defined" }
+        assertNotNull(classItem, message = "Expected $qualifiedName to be defined")
         return classItem
     }
 
     /** Get the package from the [Codebase], failing if it does not exist. */
     fun Codebase.assertPackage(pkgName: String): PackageItem {
         val packageItem = findPackage(pkgName)
-        assertNotNull(packageItem) { "Expected $pkgName to be defined" }
+        assertNotNull(packageItem, message = "Expected $pkgName to be defined")
         return packageItem
     }
 
     /** Get the field from the [ClassItem], failing if it does not exist. */
     fun ClassItem.assertField(fieldName: String): FieldItem {
         val fieldItem = findField(fieldName)
-        assertNotNull(fieldItem) { "Expected $fieldName to be defined" }
+        assertNotNull(fieldItem, message = "Expected $fieldName to be defined")
         return fieldItem
     }
 
     /** Get the method from the [ClassItem], failing if it does not exist. */
     fun ClassItem.assertMethod(methodName: String, parameters: String): MethodItem {
         val methodItem = findMethod(methodName, parameters)
-        assertNotNull(methodItem) { "Expected $methodName($parameters) to be defined" }
+        assertNotNull(methodItem, message = "Expected $methodName($parameters) to be defined")
         return methodItem
     }
 
     /** Get the constructor from the [ClassItem], failing if it does not exist. */
     fun ClassItem.assertConstructor(parameters: String): ConstructorItem {
         val methodItem = findMethod(simpleName(), parameters)
-        assertNotNull(methodItem) { "Expected ${simpleName()}($parameters) to be defined" }
+        assertNotNull(methodItem, message = "Expected ${simpleName()}($parameters) to be defined")
         return assertIs(methodItem)
     }
 
+    /** Get the property from the [ClassItem], failing if it does not exist. */
+    fun ClassItem.assertProperty(propertyName: String): PropertyItem {
+        val propertyItem = properties().firstOrNull { it.name() == propertyName }
+        assertNotNull(propertyItem, message = "Expected $propertyName to be defined")
+        return propertyItem
+    }
+
     /** Get the annotation from the [Item], failing if it does not exist. */
-    fun Item.assertAnnotation(parameters: String): AnnotationItem {
-        val annoItem =
-            modifiers.annotations().filter { it.qualifiedName == parameters }.firstOrNull()
-        assertNotNull(annoItem) { "Expected item to be annotated with ($parameters)" }
+    fun Item.assertAnnotation(qualifiedName: String): AnnotationItem {
+        val annoItem = modifiers.findAnnotation(qualifiedName)
+        assertNotNull(annoItem, message = "Expected item to be annotated with ($qualifiedName)")
         return assertIs(annoItem)
     }
+
+    /**
+     * Check to make sure that this [TypeItem] is actually a [VariableTypeItem] whose
+     * [VariableTypeItem.asTypeParameter] references the supplied [typeParameter].
+     */
+    fun TypeItem.assertReferencesTypeParameter(typeParameter: TypeParameterItem) {
+        assertThat(this).isInstanceOf(VariableTypeItem::class.java)
+        this as VariableTypeItem
+        assertThat(asTypeParameter).isSameInstanceAs(typeParameter)
+    }
 }
diff --git a/metalava-reporter/src/main/java/com/android/tools/metalava/reporter/Issues.kt b/metalava-reporter/src/main/java/com/android/tools/metalava/reporter/Issues.kt
index a29f4a8..cd5944c 100644
--- a/metalava-reporter/src/main/java/com/android/tools/metalava/reporter/Issues.kt
+++ b/metalava-reporter/src/main/java/com/android/tools/metalava/reporter/Issues.kt
@@ -225,6 +225,7 @@
     val GENERIC_CALLBACKS = Issue(Severity.ERROR, Category.API_LINT)
     val KOTLIN_DEFAULT_PARAMETER_ORDER = Issue(Severity.ERROR, Category.API_LINT_ANDROIDX_MISC)
     val UNFLAGGED_API = Issue(Severity.HIDDEN, Category.API_LINT)
+    val FLAGGED_API_LITERAL = Issue(Severity.HIDDEN, Category.API_LINT)
 
     fun findIssueById(id: String?): Issue? {
         return nameToIssue[id]
diff --git a/metalava-testing/src/main/java/com/android/tools/metalava/testing/AndroidTestUtils.kt b/metalava-testing/src/main/java/com/android/tools/metalava/testing/AndroidTestUtils.kt
index bc3e1d7..df5d477 100644
--- a/metalava-testing/src/main/java/com/android/tools/metalava/testing/AndroidTestUtils.kt
+++ b/metalava-testing/src/main/java/com/android/tools/metalava/testing/AndroidTestUtils.kt
@@ -37,8 +37,34 @@
  */
 private fun File.isMetalavaRootDir(): Boolean = resolve("metalava-model").isDirectory
 
+/** Get a [File] for the public `android.jar` of the specified [apiLevel]. */
 fun getAndroidJar(apiLevel: Int = API_LEVEL): File {
-    // This is either running in tools/metalava or tools/metalava/subproject-dir andwe need to look
+    val metalavaDir = getMetalavaDir()
+
+    val localFile = metalavaDir.resolve("../../prebuilts/sdk/$apiLevel/public/android.jar")
+    if (localFile.exists()) {
+        return localFile
+    } else {
+        val androidJar = File("../../prebuilts/sdk/$apiLevel/android.jar")
+        if (androidJar.exists()) return androidJar
+        return getAndroidJarFromEnv(apiLevel)
+    }
+}
+
+/** Get a [File] for the [apiSurface] `android.txt` of the specified [apiLevel]. */
+fun getAndroidTxt(apiLevel: Int = API_LEVEL, apiSurface: String = "public"): File {
+    val metalavaDir = getMetalavaDir()
+
+    val localFile = metalavaDir.resolve("../../prebuilts/sdk/$apiLevel/$apiSurface/api/android.txt")
+    if (!localFile.exists()) {
+        error("Missing ${localFile.absolutePath} file in the SDK")
+    }
+
+    return localFile
+}
+
+private fun getMetalavaDir(): File {
+    // This is either running in tools/metalava or tools/metalava/subproject-dir and we need to look
     // in prebuilts/sdk, so first find tools/metalava then resolve relative to that.
     val cwd = File("").absoluteFile
     val metalavaDir =
@@ -50,12 +76,5 @@
                 throw IllegalArgumentException("Could not find metalava-model in $cwd")
             }
         }
-    val localFile = metalavaDir.resolve("../../prebuilts/sdk/$apiLevel/public/android.jar")
-    if (localFile.exists()) {
-        return localFile
-    } else {
-        val androidJar = File("../../prebuilts/sdk/$apiLevel/android.jar")
-        if (androidJar.exists()) return androidJar
-        return getAndroidJarFromEnv(apiLevel)
-    }
+    return metalavaDir
 }
diff --git a/metalava-testing/src/main/java/com/android/tools/metalava/testing/KnownSourceFiles.kt b/metalava-testing/src/main/java/com/android/tools/metalava/testing/KnownSourceFiles.kt
index 1442f0a..d59d27a 100644
--- a/metalava-testing/src/main/java/com/android/tools/metalava/testing/KnownSourceFiles.kt
+++ b/metalava-testing/src/main/java/com/android/tools/metalava/testing/KnownSourceFiles.kt
@@ -132,4 +132,20 @@
         """
             )
             .indented()
+
+    val supportParameterName =
+        java(
+            """
+                package androidx.annotation;
+                import java.lang.annotation.*;
+                import static java.lang.annotation.ElementType.*;
+                import static java.lang.annotation.RetentionPolicy.SOURCE;
+                @SuppressWarnings("WeakerAccess")
+                @Retention(SOURCE)
+                @Target({METHOD, PARAMETER, FIELD})
+                public @interface ParameterName {
+                    String value();
+                }
+            """
+        )
 }
diff --git a/metalava-testing/src/main/java/com/android/tools/metalava/testing/TestUtils.kt b/metalava-testing/src/main/java/com/android/tools/metalava/testing/TestUtils.kt
index bbed726..6b673aa 100644
--- a/metalava-testing/src/main/java/com/android/tools/metalava/testing/TestUtils.kt
+++ b/metalava-testing/src/main/java/com/android/tools/metalava/testing/TestUtils.kt
@@ -18,6 +18,7 @@
 
 import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.lint.checks.infrastructure.TestFiles
+import java.io.File
 import org.intellij.lang.annotations.Language
 
 fun source(to: String, source: String): TestFile {
@@ -43,3 +44,17 @@
 fun kotlin(to: String, @Language("kotlin") source: String): TestFile {
     return TestFiles.kotlin(to, source.trimIndent())
 }
+
+/**
+ * Create a signature [TestFile] with the supplied [contents] in a file with a path of `api.txt`.
+ */
+fun signature(contents: String): TestFile {
+    return signature("api.txt", contents)
+}
+
+/** Create a signature [TestFile] with the supplied [contents] in a file with a path of [to]. */
+fun signature(to: String, contents: String): TestFile {
+    return source(to, contents.trimIndent())
+}
+
+fun List<TestFile>.createFiles(dir: File): List<File> = map { it.createFile(dir) }
diff --git a/metalava/build.gradle.kts b/metalava/build.gradle.kts
index 6336288..c26f390 100644
--- a/metalava/build.gradle.kts
+++ b/metalava/build.gradle.kts
@@ -48,7 +48,9 @@
     implementation(libs.asm)
     implementation(libs.asmTree)
     implementation(libs.gson)
+
     testImplementation(project(":metalava-testing"))
+    testImplementation(testFixtures(project(":metalava-model")))
     testImplementation(testFixtures(project(":metalava-model-text")))
     testImplementation(libs.androidLintTests)
     testImplementation(libs.junit4)
diff --git a/metalava/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt b/metalava/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt
index 044e2b2..4f4a17a 100644
--- a/metalava/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt
@@ -59,9 +59,9 @@
 import com.android.tools.metalava.model.TypeItem
 import com.android.tools.metalava.model.TypeNullability
 import com.android.tools.metalava.model.psi.PsiAnnotationItem
-import com.android.tools.metalava.model.psi.extractRoots
 import com.android.tools.metalava.model.source.SourceCodebase
 import com.android.tools.metalava.model.source.SourceParser
+import com.android.tools.metalava.model.source.SourceSet
 import com.android.tools.metalava.model.text.ApiFile
 import com.android.tools.metalava.model.text.ApiParseException
 import com.android.tools.metalava.model.visitors.ApiVisitor
@@ -130,14 +130,13 @@
             if (javaStubFiles.isNotEmpty()) {
                 // Set up class path to contain our main sources such that we can
                 // resolve types in the stubs
-                val roots = mutableListOf<File>()
-                extractRoots(reporter, options.sources, roots)
-                roots.addAll(options.sourcePath)
+                val roots =
+                    SourceSet(options.sources, options.sourcePath).extractRoots(reporter).sourcePath
                 val javaStubsCodebase =
                     sourceParser.parseSources(
-                        javaStubFiles,
+                        SourceSet(javaStubFiles, roots),
+                        SourceSet.empty(),
                         "Codebase loaded from stubs",
-                        sourcePath = roots,
                         classPath = options.classpath
                     )
                 mergeJavaStubsCodebase(javaStubsCodebase)
@@ -242,9 +241,12 @@
 
     private fun mergeAnnotationsSignatureFile(path: String) {
         try {
-            val signatureCodebase = ApiFile.parseApi(File(path), codebase.annotationManager)
-            signatureCodebase.description =
-                "Signature files for annotation merger: loaded from $path"
+            val signatureCodebase =
+                ApiFile.parseApi(
+                    File(path),
+                    codebase.annotationManager,
+                    "Signature files for annotation merger: loaded from $path"
+                )
             mergeQualifierAnnotationsFromCodebase(signatureCodebase)
         } catch (ex: ApiParseException) {
             val message = "Unable to parse signature file $path: ${ex.message}"
@@ -327,7 +329,10 @@
                 }
             }
 
-        CodebaseComparator().compare(visitor, externalCodebase, codebase)
+        CodebaseComparator(
+                apiVisitorConfig = @Suppress("DEPRECATION") options.apiVisitorConfig,
+            )
+            .compare(visitor, externalCodebase, codebase)
     }
 
     private fun mergeInclusionAnnotationsFromCodebase(externalCodebase: Codebase) {
diff --git a/metalava/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt b/metalava/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt
index 6f37802..fc292c5 100644
--- a/metalava/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt
@@ -302,16 +302,15 @@
     ) {
         if (!cls.isClass()) return
         if (cls.superClass() == null) return
-        val superClasses: Sequence<ClassItem> =
-            generateSequence(cls.superClass()) { it.superClass() }
-        val hiddenSuperClasses: Sequence<ClassItem> =
-            superClasses.filter { !filterReference.test(it) && !it.isJavaLangObject() }
+        val allSuperClasses = cls.allSuperClasses()
+        val hiddenSuperClasses =
+            allSuperClasses.filter { !filterReference.test(it) && !it.isJavaLangObject() }
 
         if (hiddenSuperClasses.none()) { // not missing any implementation methods
             return
         }
 
-        addInheritedStubsFrom(cls, hiddenSuperClasses, superClasses, filterEmit, filterReference)
+        addInheritedStubsFrom(cls, hiddenSuperClasses, allSuperClasses, filterEmit, filterReference)
         addInheritedInterfacesFrom(cls, hiddenSuperClasses, filterReference)
     }
 
@@ -320,7 +319,7 @@
         hiddenSuperClasses: Sequence<ClassItem>,
         filterReference: Predicate<Item>
     ) {
-        var interfaceTypes: MutableList<TypeItem>? = null
+        var interfaceTypes: MutableList<ClassTypeItem>? = null
         var interfaceTypeClasses: MutableList<ClassItem>? = null
         for (hiddenSuperClass in hiddenSuperClasses) {
             for (hiddenInterface in hiddenSuperClass.interfaceTypes()) {
@@ -344,7 +343,7 @@
                     if (hiddenInterfaceClass.hasTypeVariables()) {
                         val mapping = cls.mapTypeVariables(hiddenSuperClass)
                         if (mapping.isNotEmpty()) {
-                            val mappedType: TypeItem = hiddenInterface.convertType(mapping)
+                            val mappedType = hiddenInterface.convertType(mapping)
                             interfaceTypes.add(mappedType)
                             continue
                         }
@@ -414,15 +413,12 @@
             // Determine if there is a non-hidden class between the superClass and this class.
             // If non-hidden classes are found, don't include the methods for this hiddenSuperClass,
             // as it will already have been included in a previous super class
-            var includeHiddenSuperClassMethods = true
-            var currentClass = cls.superClass()
-            while (currentClass != superClass && currentClass != null) {
-                if (!hiddenSuperClasses.contains(currentClass)) {
-                    includeHiddenSuperClassMethods = false
-                    break
-                }
-                currentClass = currentClass.superClass()
-            }
+            val includeHiddenSuperClassMethods =
+                !cls.allSuperClasses()
+                    // Search from this class up to, but not including the superClass.
+                    .takeWhile { currentClass -> currentClass != superClass }
+                    // Find any class that is not hidden.
+                    .any { currentClass -> !hiddenSuperClasses.contains(currentClass) }
 
             if (!includeHiddenSuperClassMethods) {
                 continue
@@ -1261,10 +1257,7 @@
             return
         }
 
-        if (
-            (cl.isHiddenOrRemoved() || cl.isPackagePrivate && !cl.isApiCandidate()) &&
-                !cl.isTypeParameter
-        ) {
+        if (cl.isHiddenOrRemoved() || cl.isPackagePrivate && !cl.isApiCandidate()) {
             reporter.report(
                 Issues.REFERENCES_HIDDEN,
                 from,
@@ -1385,8 +1378,9 @@
                 )
             }
             for (thrown in method.throwsTypes()) {
+                if (thrown.isTypeParameter) continue
                 cantStripThis(
-                    thrown,
+                    thrown.classItem,
                     filter,
                     notStrippable,
                     stubImportPackages,
diff --git a/metalava/src/main/java/com/android/tools/metalava/ApiPredicate.kt b/metalava/src/main/java/com/android/tools/metalava/ApiPredicate.kt
index 9958612..36d9b73 100644
--- a/metalava/src/main/java/com/android/tools/metalava/ApiPredicate.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/ApiPredicate.kt
@@ -22,6 +22,7 @@
 import com.android.tools.metalava.model.MemberItem
 import com.android.tools.metalava.model.MethodItem
 import com.android.tools.metalava.model.PackageItem
+import com.android.tools.metalava.model.TypeParameterItem
 import java.util.function.Predicate
 
 /**
@@ -89,7 +90,7 @@
         }
 
         // Type Parameter references (e.g. T) aren't actual types, skip all visibility checks
-        if (member is ClassItem && member.isTypeParameter) {
+        if (member is TypeParameterItem) {
             return true
         }
 
diff --git a/metalava/src/main/java/com/android/tools/metalava/ComparisonVisitor.kt b/metalava/src/main/java/com/android/tools/metalava/ComparisonVisitor.kt
index d277221..9e514d5 100644
--- a/metalava/src/main/java/com/android/tools/metalava/ComparisonVisitor.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/ComparisonVisitor.kt
@@ -109,8 +109,7 @@
 private fun <E> Stack<E>.peek(): E = last()
 
 class CodebaseComparator(
-    @Suppress("DEPRECATION")
-    private val apiVisitorConfig: ApiVisitor.Config = options.apiVisitorConfig,
+    private val apiVisitorConfig: ApiVisitor.Config,
 ) {
     /**
      * Visits this codebase and compares it with another codebase, informing the visitors about the
diff --git a/metalava/src/main/java/com/android/tools/metalava/ConvertJarsToSignatureFiles.kt b/metalava/src/main/java/com/android/tools/metalava/ConvertJarsToSignatureFiles.kt
index da41736..7ca53ab 100644
--- a/metalava/src/main/java/com/android/tools/metalava/ConvertJarsToSignatureFiles.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/ConvertJarsToSignatureFiles.kt
@@ -138,27 +138,6 @@
                 }
             }
 
-            // Sadly the old signature files have some APIs recorded as deprecated which
-            // are not in fact deprecated in the jar files. Try to pull this back in.
-
-            val oldRemovedFile = File(root, "prebuilts/sdk/$api/public/api/removed.txt")
-            if (oldRemovedFile.isFile) {
-                val oldCodebase = signatureFileLoader.load(oldRemovedFile)
-                val visitor =
-                    object : ComparisonVisitor() {
-                        override fun compare(old: MethodItem, new: MethodItem) {
-                            new.removed = true
-                            progressTracker.progress("Removed $old")
-                        }
-
-                        override fun compare(old: FieldItem, new: FieldItem) {
-                            new.removed = true
-                            progressTracker.progress("Removed $old")
-                        }
-                    }
-                CodebaseComparator().compare(visitor, oldCodebase, jarCodebase, null)
-            }
-
             // Read deprecated attributes. Seem to be missing from code model;
             // try to read via ASM instead since it must clearly be there.
             markDeprecated(jarCodebase, apiJar, apiJar.path)
diff --git a/metalava/src/main/java/com/android/tools/metalava/DexApiWriter.kt b/metalava/src/main/java/com/android/tools/metalava/DexApiWriter.kt
index 7255667..6407e5e 100644
--- a/metalava/src/main/java/com/android/tools/metalava/DexApiWriter.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/DexApiWriter.kt
@@ -40,7 +40,7 @@
     ) {
     override fun visitClass(cls: ClassItem) {
         if (filterEmit.test(cls)) {
-            writer.print(cls.toType().internalName())
+            writer.print(cls.type().internalName())
             writer.print("\n")
         }
     }
@@ -50,7 +50,7 @@
             return
         }
 
-        writer.print(method.containingClass().toType().internalName())
+        writer.print(method.containingClass().type().internalName())
         writer.print("->")
         writer.print(method.internalName())
         writer.print("(")
@@ -70,7 +70,7 @@
     override fun visitField(field: FieldItem) {
         val cls = field.containingClass()
 
-        writer.print(cls.toType().internalName())
+        writer.print(cls.type().internalName())
         writer.print("->")
         writer.print(field.name())
         writer.print(":")
diff --git a/metalava/src/main/java/com/android/tools/metalava/Driver.kt b/metalava/src/main/java/com/android/tools/metalava/Driver.kt
index 54f2a95..652b0c0 100644
--- a/metalava/src/main/java/com/android/tools/metalava/Driver.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/Driver.kt
@@ -42,11 +42,11 @@
 import com.android.tools.metalava.model.ClassItem
 import com.android.tools.metalava.model.ClassResolver
 import com.android.tools.metalava.model.Codebase
-import com.android.tools.metalava.model.psi.gatherSources
+import com.android.tools.metalava.model.MergedCodebase
 import com.android.tools.metalava.model.source.EnvironmentManager
 import com.android.tools.metalava.model.source.SourceParser
+import com.android.tools.metalava.model.source.SourceSet
 import com.android.tools.metalava.model.text.ApiClassResolution
-import com.android.tools.metalava.model.text.TextCodebase
 import com.android.tools.metalava.model.visitors.ApiVisitor
 import com.android.tools.metalava.reporter.Issues
 import com.android.tools.metalava.reporter.Reporter
@@ -418,14 +418,13 @@
 @Suppress("DEPRECATION")
 private fun addMissingItemsRequiredForGeneratingStubs(
     sourceParser: SourceParser,
-    textCodebase: TextCodebase,
+    codebase: Codebase,
     reporterApiLint: Reporter,
 ) {
     // Reuse the existing ApiAnalyzer support for adding constructors that is used in
     // [loadFromSources], to make sure that the constructors are correct when generating stubs
     // from source files.
-    val analyzer =
-        ApiAnalyzer(sourceParser, textCodebase, reporterApiLint, options.apiAnalyzerConfig)
+    val analyzer = ApiAnalyzer(sourceParser, codebase, reporterApiLint, options.apiAnalyzerConfig)
     analyzer.addConstructors { _ -> true }
 }
 
@@ -446,7 +445,9 @@
         }
 
     @Suppress("DEPRECATION")
-    CodebaseComparator()
+    CodebaseComparator(
+            apiVisitorConfig = @Suppress("DEPRECATION") options.apiVisitorConfig,
+        )
         .compare(
             object : ComparisonVisitor() {
                 override fun compare(old: ClassItem, new: ClassItem) {
@@ -468,7 +469,6 @@
     check: CheckRequest,
 ) {
     progressTracker.progress("Checking API compatibility ($check): ")
-    val signatureFile = check.file
 
     val apiType = check.apiType
     val generatedApiFile =
@@ -488,6 +488,7 @@
     //   check the lengths first and then compare contents byte for byte so that it exits
     //   quickly if they're different and does not do all the UTF-8 conversions.
     generatedApiFile?.let { apiFile ->
+        val signatureFile = check.files.last()
         val compatibilityCheckCanBeSkipped =
             signatureFile.extension == "txt" && compareFileContents(apiFile, signatureFile)
         // TODO(b/301282006): Remove global variable use when this can be tested properly
@@ -495,11 +496,13 @@
         if (compatibilityCheckCanBeSkipped) return
     }
 
-    val oldCodebase =
-        if (signatureFile.path.endsWith(DOT_JAR)) {
-            loadFromJarFile(signatureFile)
-        } else {
-            signatureFileCache.load(signatureFile, classResolverProvider.classResolver)
+    val oldCodebases =
+        check.files.map { signatureFile ->
+            if (signatureFile.path.endsWith(DOT_JAR)) {
+                loadFromJarFile(signatureFile)
+            } else {
+                signatureFileCache.load(signatureFile, classResolverProvider.classResolver)
+            }
         }
 
     var baseApi: Codebase? = null
@@ -517,11 +520,15 @@
         )
     }
 
+    // The MergedCodebase assume that the codebases are in order from widest to narrowest which is
+    // the opposite of how they are supplied so this reverses them.
+    val mergedOldCodebases = MergedCodebase(oldCodebases.reversed())
+
     // If configured, compares the new API with the previous API and reports
     // any incompatibilities.
     CompatibilityCheck.checkCompatibility(
         newCodebase,
-        oldCodebase,
+        mergedOldCodebases,
         apiType,
         baseApi,
         options.reporterCompatibilityReleased,
@@ -594,22 +601,29 @@
 ): Codebase {
     progressTracker.progress("Processing sources: ")
 
-    val sources =
-        options.sources.ifEmpty {
+    val sourceSet =
+        if (options.sources.isEmpty()) {
             if (options.verbose) {
                 options.stdout.println(
                     "No source files specified: recursively including all sources found in the source path (${options.sourcePath.joinToString()}})"
                 )
             }
-            gatherSources(options.reporter, options.sourcePath)
+            SourceSet.createFromSourcePath(options.reporter, options.sourcePath)
+        } else {
+            SourceSet(options.sources, options.sourcePath)
         }
 
+    val commonSourceSet =
+        if (options.commonSourcePath.isNotEmpty())
+            SourceSet.createFromSourcePath(options.reporter, options.commonSourcePath)
+        else SourceSet.empty()
+
     progressTracker.progress("Reading Codebase: ")
     val codebase =
         sourceParser.parseSources(
-            sources,
+            sourceSet,
+            commonSourceSet,
             "Codebase loaded from source folders",
-            sourcePath = options.sourcePath,
             classPath = options.classpath,
         )
 
@@ -755,16 +769,6 @@
     codebase: Codebase,
     docStubs: Boolean,
 ) {
-    if (codebase is TextCodebase) {
-        if (options.verbose) {
-            options.stdout.println(
-                "Generating stubs from text based codebase is an experimental feature. " +
-                    "It is not guaranteed that stubs generated from text based codebase are " +
-                    "class level equivalent to the stubs generated from source files. "
-            )
-        }
-    }
-
     if (docStubs) {
         progressTracker.progress("Generating documentation stub files: ")
     } else {
diff --git a/metalava/src/main/java/com/android/tools/metalava/JDiffXmlWriter.kt b/metalava/src/main/java/com/android/tools/metalava/JDiffXmlWriter.kt
index 98b0f56..980d6e3 100644
--- a/metalava/src/main/java/com/android/tools/metalava/JDiffXmlWriter.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/JDiffXmlWriter.kt
@@ -26,6 +26,7 @@
 import com.android.tools.metalava.model.MethodItem
 import com.android.tools.metalava.model.PackageItem
 import com.android.tools.metalava.model.PropertyItem
+import com.android.tools.metalava.model.ThrowableType
 import com.android.tools.metalava.model.TypeItem
 import com.android.tools.metalava.model.psi.CodePrinter
 import com.android.tools.metalava.model.visitors.ApiVisitor
@@ -300,7 +301,7 @@
                 else -> method.filteredThrowsTypes(filterReference).asSequence()
             }
         if (throws.any()) {
-            throws.sortedWith(ClassItem.fullNameComparator).forEach { type ->
+            throws.sortedWith(ThrowableType.fullNameComparator).forEach { type ->
                 writer.print("<exception name=\"")
                 writer.print(type.fullName())
                 writer.print("\" type=\"")
diff --git a/metalava/src/main/java/com/android/tools/metalava/NullnessMigration.kt b/metalava/src/main/java/com/android/tools/metalava/NullnessMigration.kt
index dc727bf..2e3b050 100644
--- a/metalava/src/main/java/com/android/tools/metalava/NullnessMigration.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/NullnessMigration.kt
@@ -93,7 +93,10 @@
 
     companion object {
         fun migrateNulls(codebase: Codebase, previous: Codebase) {
-            CodebaseComparator().compare(NullnessMigration(), previous, codebase)
+            CodebaseComparator(
+                    apiVisitorConfig = @Suppress("DEPRECATION") options.apiVisitorConfig,
+                )
+                .compare(NullnessMigration(), previous, codebase)
         }
 
         fun hasNullnessInformation(item: Item): Boolean {
diff --git a/metalava/src/main/java/com/android/tools/metalava/Options.kt b/metalava/src/main/java/com/android/tools/metalava/Options.kt
index 6c6a6f7..89b4d26 100644
--- a/metalava/src/main/java/com/android/tools/metalava/Options.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/Options.kt
@@ -136,6 +136,7 @@
 private const val INDENT_WIDTH = 45
 
 const val ARG_CLASS_PATH = "--classpath"
+const val ARG_COMMON_SOURCE_PATH = "--common-source-path"
 const val ARG_SOURCE_PATH = "--source-path"
 const val ARG_SOURCE_FILES = "--source-files"
 const val ARG_XML_API = "--api-xml"
@@ -231,6 +232,8 @@
 
     /** Internal list backing [sources] */
     private val mutableSources: MutableList<File> = mutableListOf()
+    /** Internal list backing [commonSourcePath] */
+    private val mutableCommonSourcePath: MutableList<File> = mutableListOf()
     /** Internal list backing [sourcePath] */
     private val mutableSourcePath: MutableList<File> = mutableListOf()
     /** Internal list backing [classpath] */
@@ -330,6 +333,9 @@
     /** If true, treat all API lint warnings as errors */
     var lintsAreErrors: Boolean = false
 
+    /** Ths list of source roots in the common module */
+    val commonSourcePath: List<File> = mutableCommonSourcePath
+
     /** The list of source roots */
     val sourcePath: List<File> = mutableSourcePath
 
@@ -441,7 +447,7 @@
                 ARG_SUPPRESS_COMPATIBILITY_META_ANNOTATION,
                 help =
                     """
-                       Suppress compatibility checks for any elements within the scope of an 
+                       Suppress compatibility checks for any elements within the scope of an
                        annotation which is itself annotated with the given meta-annotation.
                     """
                         .trimIndent(),
@@ -805,6 +811,21 @@
                 else -> error("Internal error: Invalid flag: $flag")
             }
 
+        fun getSourcePath(path: String, arg: String, sourcePathToStore: MutableList<File>) {
+            if (path.isBlank()) {
+                // Don't compute absolute path; we want to skip this file later on.
+                // For current directory one should use ".", not "".
+                sourcePathToStore.add(File(""))
+            } else {
+                if (path.endsWith(SdkConstants.DOT_JAVA)) {
+                    throw MetalavaCliException(
+                        "$arg should point to a source root directory, not a source file ($path)"
+                    )
+                }
+                sourcePathToStore.addAll(stringToExistingDirsOrJars(path))
+            }
+        }
+
         var index = 0
         while (index < args.size) {
             when (val arg = args[index]) {
@@ -813,22 +834,15 @@
                     val path = getValue(args, ++index)
                     mutableClassPath.addAll(stringToExistingDirsOrJars(path))
                 }
+                ARG_COMMON_SOURCE_PATH -> {
+                    val path = getValue(args, ++index)
+                    getSourcePath(path, arg, mutableCommonSourcePath)
+                }
                 ARG_SOURCE_PATH,
                 "--sources",
                 "--sourcepath" -> {
                     val path = getValue(args, ++index)
-                    if (path.isBlank()) {
-                        // Don't compute absolute path; we want to skip this file later on.
-                        // For current directory one should use ".", not "".
-                        mutableSourcePath.add(File(""))
-                    } else {
-                        if (path.endsWith(SdkConstants.DOT_JAVA)) {
-                            throw MetalavaCliException(
-                                "$arg should point to a source root directory, not a source file ($path)"
-                            )
-                        }
-                        mutableSourcePath.addAll(stringToExistingDirsOrJars(path))
-                    }
+                    getSourcePath(path, arg, mutableSourcePath)
                 }
                 ARG_SOURCE_FILES -> {
                     val listString = getValue(args, ++index)
@@ -1483,6 +1497,11 @@
                 "$ARG_SOURCE_PATH <paths>",
                 "One or more directories (separated by `${File.pathSeparator}`) " +
                     "containing source files (within a package hierarchy).",
+                "$ARG_COMMON_SOURCE_PATH <paths>",
+                "One or more directories (separated by `${File.pathSeparator}`) " +
+                    "containing common source files (within a package hierarchy) " +
+                    "where platform-agnostic `expect` declarations as well as " +
+                    "common business logic are defined.",
                 "$ARG_CLASS_PATH <paths>",
                 "One or more directories or jars (separated by " +
                     "`${File.pathSeparator}`) containing classes that should be on the classpath when parsing the " +
diff --git a/metalava/src/main/java/com/android/tools/metalava/SdkFileWriter.kt b/metalava/src/main/java/com/android/tools/metalava/SdkFileWriter.kt
index e65e774..ceb2e21 100644
--- a/metalava/src/main/java/com/android/tools/metalava/SdkFileWriter.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/SdkFileWriter.kt
@@ -156,18 +156,19 @@
         // are enclosed by a layout class (and not one that has been declared as a widget)
         var i = 0
         while (i < layoutParams.size) {
-            var clazz: ClassItem? = layoutParams[i]
-            val containingClass = clazz?.containingClass()
-            var remove = containingClass == null || layouts.indexOf(containingClass) == -1
-            // Also ensure that super classes of the layout params are in android.widget or
-            // android.view.
-            while (!remove && clazz != null) {
-                clazz = clazz.superClass() ?: break
-                if (clazz == topLayoutParams) {
-                    break
-                }
-                remove = !isIncludedPackage(clazz)
-            }
+            val clazz = layoutParams[i]
+            val containingClass = clazz.containingClass()
+            val remove =
+                containingClass == null ||
+                    layouts.indexOf(containingClass) == -1 ||
+                    // Also ensure that super classes of the layout params are in android.widget or
+                    // android.view.
+                    clazz
+                        .allSuperClasses()
+                        // Search up to but not including topLayoutParams
+                        .takeWhile { clazz != topLayoutParams }
+                        // Find any class that is not in the widget or view packages.
+                        .any { !isIncludedPackage(clazz) }
             if (remove) {
                 layoutParams.removeAt(i)
             } else {
@@ -266,10 +267,8 @@
     @Throws(IOException::class)
     private fun writeClass(writer: BufferedWriter, clazz: ClassItem, prefix: Char) {
         writer.append(prefix).append(clazz.qualifiedName())
-        var superClass: ClassItem? = clazz.superClass()
-        while (superClass != null) {
+        for (superClass in clazz.allSuperClasses()) {
             writer.append(' ').append(superClass.qualifiedName())
-            superClass = superClass.superClass()
         }
         writer.append('\n')
     }
diff --git a/metalava/src/main/java/com/android/tools/metalava/SignatureFileCache.kt b/metalava/src/main/java/com/android/tools/metalava/SignatureFileCache.kt
index 254e048..9ab77e1 100644
--- a/metalava/src/main/java/com/android/tools/metalava/SignatureFileCache.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/SignatureFileCache.kt
@@ -19,18 +19,30 @@
 import com.android.tools.metalava.cli.common.SignatureFileLoader
 import com.android.tools.metalava.model.AnnotationManager
 import com.android.tools.metalava.model.ClassResolver
-import com.android.tools.metalava.model.text.TextCodebase
+import com.android.tools.metalava.model.Codebase
 import java.io.File
 
-private data class CacheKey(val file: File, val classResolver: ClassResolver?)
+private data class CacheKey(val files: List<File>, val classResolver: ClassResolver?) {
+    fun load(signatureFileLoader: SignatureFileLoader): Codebase =
+        if (files.size == 1) {
+            signatureFileLoader.load(files.single(), classResolver)
+        } else {
+            signatureFileLoader.loadFiles(files, classResolver)
+        }
+}
 
 /** Loads signature files, caching them for reuse where appropriate. */
 class SignatureFileCache(annotationManager: AnnotationManager) {
     private val signatureFileLoader = SignatureFileLoader(annotationManager)
-    private val map = mutableMapOf<CacheKey, TextCodebase>()
+    private val map = mutableMapOf<CacheKey, Codebase>()
 
-    fun load(file: File, classResolver: ClassResolver? = null): TextCodebase {
-        val key = CacheKey(file, classResolver)
-        return map.computeIfAbsent(key) { k -> signatureFileLoader.load(k.file, k.classResolver) }
+    fun load(file: File, classResolver: ClassResolver? = null): Codebase =
+        load(listOf(file), classResolver)
+
+    fun load(files: List<File>, classResolver: ClassResolver? = null): Codebase {
+        val key = CacheKey(files, classResolver)
+        return map.computeIfAbsent(key) { k ->
+            signatureFileLoader.loadFiles(k.files, k.classResolver)
+        }
     }
 }
diff --git a/metalava/src/main/java/com/android/tools/metalava/SignatureWriter.kt b/metalava/src/main/java/com/android/tools/metalava/SignatureWriter.kt
index 6e1c921..4c5ae87 100644
--- a/metalava/src/main/java/com/android/tools/metalava/SignatureWriter.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/SignatureWriter.kt
@@ -24,6 +24,7 @@
 import com.android.tools.metalava.model.ModifierListWriter
 import com.android.tools.metalava.model.PackageItem
 import com.android.tools.metalava.model.PropertyItem
+import com.android.tools.metalava.model.ThrowableType
 import com.android.tools.metalava.model.TypeItem
 import com.android.tools.metalava.model.TypeParameterList
 import com.android.tools.metalava.model.text.FileFormat
@@ -392,7 +393,9 @@
             }
         if (throws.any()) {
             write(" throws ")
-            throws.asSequence().sortedWith(ClassItem.fullNameComparator).forEachIndexed { i, type ->
+            throws.asSequence().sortedWith(ThrowableType.fullNameComparator).forEachIndexed {
+                i,
+                type ->
                 if (i > 0) {
                     write(", ")
                 }
diff --git a/metalava/src/main/java/com/android/tools/metalava/StubGenerationOptions.kt b/metalava/src/main/java/com/android/tools/metalava/StubGenerationOptions.kt
index c069e4e..237a540 100644
--- a/metalava/src/main/java/com/android/tools/metalava/StubGenerationOptions.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/StubGenerationOptions.kt
@@ -43,7 +43,7 @@
                 help =
                     """
                         Base directory to output the generated stub source files for the API, if
-                        specified.  
+                        specified.
                     """
                         .trimIndent(),
             )
diff --git a/metalava/src/main/java/com/android/tools/metalava/apilevels/AddApisFromCodebase.kt b/metalava/src/main/java/com/android/tools/metalava/apilevels/AddApisFromCodebase.kt
index fd0e1dd..74e3900 100644
--- a/metalava/src/main/java/com/android/tools/metalava/apilevels/AddApisFromCodebase.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/apilevels/AddApisFromCodebase.kt
@@ -233,7 +233,7 @@
             containingClass().containingClass() != null &&
             !containingClass().modifiers.isStatic()
     ) {
-        sb.append(containingClass().containingClass()?.toType()?.internalName() ?: "")
+        sb.append(containingClass().containingClass()?.type()?.internalName() ?: "")
     }
 
     for (parameter in parameters()) {
diff --git a/metalava/src/main/java/com/android/tools/metalava/cli/common/SignatureFileLoader.kt b/metalava/src/main/java/com/android/tools/metalava/cli/common/SignatureFileLoader.kt
index 468fe6f..997464a 100644
--- a/metalava/src/main/java/com/android/tools/metalava/cli/common/SignatureFileLoader.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/cli/common/SignatureFileLoader.kt
@@ -18,10 +18,10 @@
 
 import com.android.tools.metalava.model.AnnotationManager
 import com.android.tools.metalava.model.ClassResolver
+import com.android.tools.metalava.model.Codebase
 import com.android.tools.metalava.model.text.ApiFile
 import com.android.tools.metalava.model.text.ApiParseException
 import com.android.tools.metalava.model.text.FileFormat
-import com.android.tools.metalava.model.text.TextCodebase
 import java.io.File
 
 /**
@@ -35,18 +35,23 @@
     fun load(
         file: File,
         classResolver: ClassResolver? = null,
-    ): TextCodebase {
+    ): Codebase {
         return loadFiles(listOf(file), classResolver)
     }
 
     fun loadFiles(
         files: List<File>,
         classResolver: ClassResolver? = null,
-    ): TextCodebase {
+    ): Codebase {
         require(files.isNotEmpty()) { "files must not be empty" }
 
         try {
-            return ApiFile.parseApi(files, annotationManager, classResolver, formatForLegacyFiles)
+            return ApiFile.parseApi(
+                files = files,
+                annotationManager = annotationManager,
+                classResolver = classResolver,
+                formatForLegacyFiles = formatForLegacyFiles,
+            )
         } catch (ex: ApiParseException) {
             throw MetalavaCliException("Unable to parse signature file: ${ex.message}")
         }
diff --git a/metalava/src/main/java/com/android/tools/metalava/cli/compatibility/CompatibilityCheckOptions.kt b/metalava/src/main/java/com/android/tools/metalava/cli/compatibility/CompatibilityCheckOptions.kt
index 23e6b2b..be292e8 100644
--- a/metalava/src/main/java/com/android/tools/metalava/cli/compatibility/CompatibilityCheckOptions.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/cli/compatibility/CompatibilityCheckOptions.kt
@@ -23,6 +23,7 @@
 import com.android.tools.metalava.cli.common.map
 import com.android.tools.metalava.model.Codebase
 import com.github.ajalt.clikt.parameters.groups.OptionGroup
+import com.github.ajalt.clikt.parameters.options.multiple
 import com.github.ajalt.clikt.parameters.options.option
 import java.io.File
 
@@ -67,10 +68,16 @@
                 help =
                     """
                         Check compatibility of the previously released API.
+
+                        When multiple files are provided any files that are a delta on another file
+                        must come after the other file, e.g. if `system` is a delta on `public` then
+                        `public` must come first, then `system`. Or, in other words, they must be
+                        provided in order from the narrowest API to the widest API.
                     """
                         .trimIndent(),
             )
             .existingFile()
+            .multiple()
             .allowStructuredOptionName()
             .map { CheckRequest.optionalCheckRequest(it, ApiType.PUBLIC_API) }
 
@@ -80,10 +87,16 @@
                 help =
                     """
                         Check compatibility of the previously released but since removed APIs.
+
+                        When multiple files are provided any files that are a delta on another file
+                        must come after the other file, e.g. if `system` is a delta on `public` then
+                        `public` must come first, then `system`. Or, in other words, they must be
+                        provided in order from the narrowest API to the widest API.
                     """
                         .trimIndent(),
             )
             .existingFile()
+            .multiple()
             .allowStructuredOptionName()
             .map { CheckRequest.optionalCheckRequest(it, ApiType.REMOVED) }
 
@@ -106,20 +119,20 @@
             .allowStructuredOptionName()
 
     /**
-     * Request for compatibility checks. [file] represents the signature file to be checked.
+     * Request for compatibility checks. [files] represents the signature files to be checked.
      * [apiType] represents which part of the API should be checked.
      */
-    data class CheckRequest(val file: File, val apiType: ApiType) {
+    data class CheckRequest(val files: List<File>, val apiType: ApiType) {
 
         companion object {
-            /** Create a [CheckRequest] if the [file] is not-null, otherwise return `null`. */
-            internal fun optionalCheckRequest(file: File?, apiType: ApiType) =
-                file?.let { CheckRequest(it, apiType) }
+            /** Create a [CheckRequest] if [files] is not empty, otherwise return `null`. */
+            internal fun optionalCheckRequest(files: List<File>, apiType: ApiType) =
+                if (files.isEmpty()) null else CheckRequest(files, apiType)
         }
 
         override fun toString(): String {
             // This is only used when reporting progress.
-            return "--check-compatibility:${apiType.flagName}:released $file"
+            return "--check-compatibility:${apiType.flagName}:released $files"
         }
     }
 
@@ -131,5 +144,5 @@
 
     /** The list of [Codebase]s corresponding to [compatibilityChecks]. */
     fun previouslyReleasedCodebases(signatureFileCache: SignatureFileCache): List<Codebase> =
-        compatibilityChecks.map { signatureFileCache.load(it.file) }
+        compatibilityChecks.flatMap { it.files.map { signatureFileCache.load(it) } }
 }
diff --git a/metalava/src/main/java/com/android/tools/metalava/cli/help/HelpCommand.kt b/metalava/src/main/java/com/android/tools/metalava/cli/help/HelpCommand.kt
index 36b4ced..c3cbe98 100644
--- a/metalava/src/main/java/com/android/tools/metalava/cli/help/HelpCommand.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/cli/help/HelpCommand.kt
@@ -126,11 +126,11 @@
   signature files. Applies to the contents of the files specified on `--api` and `--removed-api`.
 
   `source` - preserves the order in which overloaded methods appear in the source files. This means
-   that refactorings of the source files which change the order but not the API can cause 
+   that refactorings of the source files which change the order but not the API can cause
    unnecessary changes in the API signature files.
 
-  `signature` (default) - sorts overloaded methods by their signature. This means that refactorings 
-  of the source files which change the order but not the API will have no effect on the API 
+  `signature` (default) - sorts overloaded methods by their signature. This means that refactorings
+  of the source files which change the order but not the API will have no effect on the API
   signature files.
 
 Currently, metalava supports the following versions:
diff --git a/metalava/src/main/java/com/android/tools/metalava/cli/signature/SignatureToJDiffCommand.kt b/metalava/src/main/java/com/android/tools/metalava/cli/signature/SignatureToJDiffCommand.kt
index 709982b..c4428b6 100644
--- a/metalava/src/main/java/com/android/tools/metalava/cli/signature/SignatureToJDiffCommand.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/cli/signature/SignatureToJDiffCommand.kt
@@ -29,25 +29,14 @@
 import com.android.tools.metalava.cli.common.progressTracker
 import com.android.tools.metalava.createReportFile
 import com.android.tools.metalava.model.ClassItem
-import com.android.tools.metalava.model.ClassResolver
 import com.android.tools.metalava.model.Codebase
 import com.android.tools.metalava.model.ConstructorItem
-import com.android.tools.metalava.model.DefaultModifierList
 import com.android.tools.metalava.model.FieldItem
 import com.android.tools.metalava.model.MethodItem
 import com.android.tools.metalava.model.PackageItem
 import com.android.tools.metalava.model.PropertyItem
 import com.android.tools.metalava.model.text.FileFormat
-import com.android.tools.metalava.model.text.ReferenceResolver
-import com.android.tools.metalava.model.text.ResolverContext
-import com.android.tools.metalava.model.text.SourcePositionInfo
-import com.android.tools.metalava.model.text.TextClassItem
-import com.android.tools.metalava.model.text.TextCodebase
-import com.android.tools.metalava.model.text.TextConstructorItem
-import com.android.tools.metalava.model.text.TextFieldItem
-import com.android.tools.metalava.model.text.TextMethodItem
-import com.android.tools.metalava.model.text.TextPackageItem
-import com.android.tools.metalava.model.text.TextPropertyItem
+import com.android.tools.metalava.model.text.TextCodebaseBuilder
 import com.android.tools.metalava.model.visitors.ApiVisitor
 import com.github.ajalt.clikt.parameters.arguments.argument
 import com.github.ajalt.clikt.parameters.options.convert
@@ -183,7 +172,7 @@
 }
 
 /**
- * Create a [TextCodebase] that is a delta between [baseApi] and [signatureApi], i.e. it includes
+ * Create a text [Codebase] that is a delta between [baseApi] and [signatureApi], i.e. it includes
  * all the [Item] that are in [signatureApi] but not in [baseApi].
  *
  * This is expected to be used where [signatureApi] is a super set of [baseApi] but that is not
@@ -205,104 +194,41 @@
     baseApi: Codebase,
     signatureApi: Codebase,
     apiVisitorConfig: ApiVisitor.Config,
-): TextCodebase {
+): Codebase {
     // Compute just the delta
-    val delta = TextCodebase(baseFile, signatureApi.annotationManager)
-    delta.description = "Delta between $baseApi and $signatureApi"
+    return TextCodebaseBuilder.build(baseFile, signatureApi.annotationManager) {
+        description = "Delta between $baseApi and $signatureApi"
 
-    CodebaseComparator(apiVisitorConfig = apiVisitorConfig)
-        .compare(
-            object : ComparisonVisitor() {
-                override fun added(new: PackageItem) {
-                    delta.addPackage(new as TextPackageItem)
-                }
-
-                override fun added(new: ClassItem) {
-                    val pkg = getOrAddPackage(new.containingPackage().qualifiedName())
-                    pkg.addClass(new as TextClassItem)
-                }
-
-                override fun added(new: ConstructorItem) {
-                    val cls = getOrAddClass(new.containingClass())
-                    cls.addConstructor(new as TextConstructorItem)
-                }
-
-                override fun added(new: MethodItem) {
-                    val cls = getOrAddClass(new.containingClass())
-                    cls.addMethod(new as TextMethodItem)
-                }
-
-                override fun added(new: FieldItem) {
-                    val cls = getOrAddClass(new.containingClass())
-                    cls.addField(new as TextFieldItem)
-                }
-
-                override fun added(new: PropertyItem) {
-                    val cls = getOrAddClass(new.containingClass())
-                    cls.addProperty(new as TextPropertyItem)
-                }
-
-                private fun getOrAddClass(fullClass: ClassItem): TextClassItem {
-                    val cls = delta.findClass(fullClass.qualifiedName())
-                    if (cls != null) {
-                        return cls
+        CodebaseComparator(apiVisitorConfig = apiVisitorConfig)
+            .compare(
+                object : ComparisonVisitor() {
+                    override fun added(new: PackageItem) {
+                        addPackage(new)
                     }
-                    val textClass = fullClass as TextClassItem
-                    val newClass =
-                        TextClassItem(
-                            delta,
-                            SourcePositionInfo.UNKNOWN,
-                            textClass.modifiers,
-                            textClass.isInterface(),
-                            textClass.isEnum(),
-                            textClass.isAnnotationType(),
-                            textClass.qualifiedName,
-                            textClass.qualifiedName,
-                            textClass.name,
-                            textClass.annotations,
-                            textClass.typeParameterList
-                        )
-                    val pkg = getOrAddPackage(fullClass.containingPackage().qualifiedName())
-                    pkg.addClass(newClass)
-                    newClass.setContainingPackage(pkg)
-                    delta.registerClass(newClass)
-                    return newClass
-                }
 
-                private fun getOrAddPackage(pkgName: String): TextPackageItem {
-                    val pkg = delta.findPackage(pkgName)
-                    if (pkg != null) {
-                        return pkg
+                    override fun added(new: ClassItem) {
+                        addClass(new)
                     }
-                    val newPkg =
-                        TextPackageItem(
-                            delta,
-                            pkgName,
-                            DefaultModifierList(delta, DefaultModifierList.PUBLIC),
-                            SourcePositionInfo.UNKNOWN
-                        )
-                    delta.addPackage(newPkg)
-                    return newPkg
-                }
-            },
-            baseApi,
-            signatureApi,
-            ApiType.ALL.getReferenceFilter(apiVisitorConfig.apiPredicateConfig)
-        )
 
-    // As the delta has not been created by the parser there is no parser provided
-    // context to use so just use an empty context.
-    val context =
-        object : ResolverContext {
-            override fun namesOfInterfaces(cl: TextClassItem): List<String>? = null
+                    override fun added(new: ConstructorItem) {
+                        addConstructor(new)
+                    }
 
-            override fun nameOfSuperClass(cl: TextClassItem): String? = null
+                    override fun added(new: MethodItem) {
+                        addMethod(new)
+                    }
 
-            override val classResolver: ClassResolver? = null
-        }
+                    override fun added(new: FieldItem) {
+                        addField(new)
+                    }
 
-    // All this actually does is add in an appropriate super class depending on the class
-    // type.
-    ReferenceResolver.resolveReferences(context, delta)
-    return delta
+                    override fun added(new: PropertyItem) {
+                        addProperty(new)
+                    }
+                },
+                baseApi,
+                signatureApi,
+                ApiType.ALL.getReferenceFilter(apiVisitorConfig.apiPredicateConfig)
+            )
+    }
 }
diff --git a/metalava/src/main/java/com/android/tools/metalava/compatibility/CompatibilityCheck.kt b/metalava/src/main/java/com/android/tools/metalava/compatibility/CompatibilityCheck.kt
index d43a7ee..e06264a 100644
--- a/metalava/src/main/java/com/android/tools/metalava/compatibility/CompatibilityCheck.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/compatibility/CompatibilityCheck.kt
@@ -609,21 +609,29 @@
         }
         */
 
-        for (exception in old.throwsTypes()) {
-            if (!new.throws(exception.qualifiedName())) {
+        for (throwType in old.throwsTypes()) {
+            // Get the throwable class, if none could be found then it is either because there is an
+            // error in the codebase or the codebase is incomplete, either way reporting an error
+            // would be unhelpful.
+            val throwableClass = throwType.throwableClass ?: continue
+            if (!new.throws(throwableClass.qualifiedName())) {
                 // exclude 'throws' changes to finalize() overrides with no arguments
                 if (old.name() != "finalize" || old.parameters().isNotEmpty()) {
                     report(
                         Issues.CHANGED_THROWS,
                         new,
-                        "${describe(new, capitalize = true)} no longer throws exception ${exception.qualifiedName()}"
+                        "${describe(new, capitalize = true)} no longer throws exception ${throwType.description()}"
                     )
                 }
             }
         }
 
-        for (exec in new.filteredThrowsTypes(filterReference)) {
-            if (!old.throws(exec.qualifiedName())) {
+        for (throwType in new.filteredThrowsTypes(filterReference)) {
+            // Get the throwable class, if none could be found then it is either because there is an
+            // error in the codebase or the codebase is incomplete, either way reporting an error
+            // would be unhelpful.
+            val throwableClass = throwType.throwableClass ?: continue
+            if (!old.throws(throwableClass.qualifiedName())) {
                 // exclude 'throws' changes to finalize() overrides with no arguments
                 if (
                     !(old.name() == "finalize" && old.parameters().isEmpty()) &&
@@ -632,7 +640,7 @@
                         !old.isEnumSyntheticMethod()
                 ) {
                     val message =
-                        "${describe(new, capitalize = true)} added thrown exception ${exec.qualifiedName()}"
+                        "${describe(new, capitalize = true)} added thrown exception ${throwType.description()}"
                     report(Issues.CHANGED_THROWS, new, message)
                 }
             }
@@ -650,7 +658,7 @@
                         "${describe(
                         new,
                         capitalize = true
-                    )} made type variable ${newTypes[i].simpleName()} reified: incompatible change"
+                    )} made type variable ${newTypes[i].name()} reified: incompatible change"
                     report(Issues.ADDED_REIFIED, new, message)
                 }
             }
@@ -988,7 +996,7 @@
         @Suppress("DEPRECATION")
         fun checkCompatibility(
             newCodebase: Codebase,
-            oldCodebase: Codebase,
+            oldCodebases: MergedCodebase,
             apiType: ApiType,
             baseApi: Codebase?,
             reporter: Reporter,
@@ -1011,19 +1019,22 @@
 
             val oldFullCodebase =
                 if (options.showUnannotated && apiType == ApiType.PUBLIC_API) {
-                    MergedCodebase(listOfNotNull(oldCodebase, baseApi))
+                    baseApi?.let { MergedCodebase(oldCodebases.children + baseApi) } ?: oldCodebases
                 } else {
                     // To avoid issues with partial oldCodeBase we fill gaps with newCodebase, the
                     // first parameter is master, so we don't change values of oldCodeBase
-                    MergedCodebase(listOfNotNull(oldCodebase, newCodebase))
+                    MergedCodebase(oldCodebases.children + newCodebase)
                 }
             val newFullCodebase = MergedCodebase(listOfNotNull(newCodebase, baseApi))
 
-            CodebaseComparator().compare(checker, oldFullCodebase, newFullCodebase, filter)
+            CodebaseComparator(
+                    apiVisitorConfig = @Suppress("DEPRECATION") options.apiVisitorConfig,
+                )
+                .compare(checker, oldFullCodebase, newFullCodebase, filter)
 
             val message =
                 "Found compatibility problems checking " +
-                    "the ${apiType.displayName} API (${newCodebase.location}) against the API in ${oldCodebase.location}"
+                    "the ${apiType.displayName} API (${newCodebase.location}) against the API in ${oldCodebases.children.last().location}"
 
             if (checker.foundProblems) {
                 throw MetalavaCliException(exitCode = -1, stderr = message)
diff --git a/metalava/src/main/java/com/android/tools/metalava/lint/ApiLint.kt b/metalava/src/main/java/com/android/tools/metalava/lint/ApiLint.kt
index 5b92b03..e19f4b3 100644
--- a/metalava/src/main/java/com/android/tools/metalava/lint/ApiLint.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/lint/ApiLint.kt
@@ -56,6 +56,7 @@
 import com.android.tools.metalava.model.AnnotationItem
 import com.android.tools.metalava.model.ArrayTypeItem
 import com.android.tools.metalava.model.ClassItem
+import com.android.tools.metalava.model.ClassTypeItem
 import com.android.tools.metalava.model.Codebase
 import com.android.tools.metalava.model.ConstructorItem
 import com.android.tools.metalava.model.FieldItem
@@ -102,6 +103,7 @@
 import com.android.tools.metalava.reporter.Issues.EXCEPTION_NAME
 import com.android.tools.metalava.reporter.Issues.EXECUTOR_REGISTRATION
 import com.android.tools.metalava.reporter.Issues.EXTENDS_ERROR
+import com.android.tools.metalava.reporter.Issues.FLAGGED_API_LITERAL
 import com.android.tools.metalava.reporter.Issues.FORBIDDEN_SUPER_CLASS
 import com.android.tools.metalava.reporter.Issues.FRACTION_FLOAT
 import com.android.tools.metalava.reporter.Issues.GENERIC_CALLBACKS
@@ -178,6 +180,7 @@
 import com.intellij.psi.PsiThisExpression
 import java.util.Locale
 import java.util.function.Predicate
+import org.jetbrains.kotlin.util.capitalizeDecapitalize.toUpperCaseAsciiOnly
 import org.jetbrains.uast.UCallExpression
 import org.jetbrains.uast.UClassLiteralExpression
 import org.jetbrains.uast.UMethod
@@ -248,7 +251,9 @@
     private fun check() {
         if (oldCodebase != null) {
             // Only check the new APIs
-            CodebaseComparator()
+            CodebaseComparator(
+                    apiVisitorConfig = @Suppress("DEPRECATION") options.apiVisitorConfig,
+                )
                 .compare(
                     object : ComparisonVisitor() {
                         override fun added(new: Item) {
@@ -364,6 +369,7 @@
         checkExtends(cls)
         checkTypedef(cls)
         checkHasFlaggedApi(cls)
+        checkFlaggedApiLiteral(cls)
     }
 
     private fun checkField(field: FieldItem) {
@@ -379,6 +385,7 @@
         checkSettingKeys(field)
         checkNullableCollections(field.type(), field)
         checkHasFlaggedApi(field)
+        checkFlaggedApiLiteral(field)
     }
 
     private fun checkMethod(method: MethodItem, filterReference: Predicate<Item>) {
@@ -396,6 +403,48 @@
         checkContextFirst(method)
         checkListenerLast(method)
         checkHasFlaggedApi(method)
+        checkFlaggedApiLiteral(method)
+    }
+
+    private fun checkFlaggedApiLiteral(item: Item) {
+        if (item.codebase.preFiltered) {
+            // Flag constants aren't ever API, so prefiltered codebases would always only contain
+            // literals.
+            return
+        }
+
+        val annotation =
+            item.modifiers.findAnnotation { it.qualifiedName == ANDROID_FLAGGED_API } ?: return
+        val attr = annotation.attributes.find { attr -> attr.name == "value" } ?: return
+
+        if (attr.value.resolve() == null) {
+            val value = attr.value.value() as? String
+            if (value == attr.value.toSource()) {
+                // For a string literal, source and value are never the same, so this happens only
+                // when a reference isn't resolvable.
+                return
+            }
+
+            val field = value?.let { aconfigFlagLiteralToFieldOrNull(item.codebase, it) }
+
+            val replacement =
+                if (field != null) {
+                    val (fieldSource, fieldItem) = field
+                    if (fieldItem != null) {
+                        fieldSource
+                    } else {
+                        "$fieldSource, however this flag doesn't seem to exist"
+                    }
+                } else {
+                    "furthermore, the current flag literal seems to be malformed"
+                }
+
+            report(
+                FLAGGED_API_LITERAL,
+                item,
+                "@FlaggedApi contains a string literal, but should reference the field generated by aconfig ($replacement).",
+            )
+        }
     }
 
     private fun checkEnums(cls: ClassItem) {
@@ -1139,7 +1188,7 @@
         // Maps each setter to a list of potential getters that would satisfy it.
         val expectedGetters = mutableListOf<Pair<Item, Set<String>>>()
         var builtType: TypeItem? = null
-        val clsType = cls.toType()
+        val clsType = cls.type()
 
         for (method in methods) {
             val name = method.name()
@@ -1656,12 +1705,20 @@
     }
 
     private fun checkExceptions(method: MethodItem, filterReference: Predicate<Item>) {
-        for (exception in method.filteredThrowsTypes(filterReference)) {
+        for (throwableType in method.filteredThrowsTypes(filterReference)) {
             if (method.isEnumSyntheticMethod()) continue
-            if (isUncheckedException(exception)) {
+            // Get the throwable class, which for a type parameter will be the lower bound. A
+            // method that throws a type parameter is treated as if it throws its lower bound, so
+            // it makes sense for this check to treat it as if it was replaced with its lower bound.
+            val throwableClass = throwableType.throwableClass ?: continue
+            if (isUncheckedException(throwableClass)) {
                 report(BANNED_THROW, method, "Methods must not throw unchecked exceptions")
+            } else if (throwableType.isTypeParameter) {
+                // Preserve legacy behavior where the following check did nothing for type
+                // parameters as a type parameters qualifiedName(), which is just its name without
+                // any package or containing class could never match a qualified exception name.
             } else {
-                when (val qualifiedName = exception.qualifiedName()) {
+                when (val qualifiedName = throwableClass.qualifiedName()) {
                     "java.lang.Exception",
                     "java.lang.Throwable",
                     "java.lang.Error" -> {
@@ -1965,7 +2022,9 @@
             }
         }
 
-        val qualifiedName = type.asClass()?.qualifiedName() ?: return
+        // Only report issues with an actual type and not a generic type that extends Number as
+        // there is nothing that can be done to avoid auto-boxing when using generic types.
+        val qualifiedName = (type as? ClassTypeItem)?.asClass()?.qualifiedName() ?: return
         if (isBoxType(qualifiedName)) {
             report(AUTO_BOXING, item, "Must avoid boxed primitives (`$qualifiedName`)")
         }
@@ -3256,6 +3315,38 @@
                     }
             }
         }
+
+        /**
+         * Heuristically converts the given string [literal] into a reference to the equivalent
+         * `aconfig`-generated `Flags.java` field.
+         *
+         * @return a pair of the field reference as Java / Kotlin source, and the referenced field
+         *   item (if found in [codebase]); or `null` if the literal cannot be converted.
+         */
+        private fun aconfigFlagLiteralToFieldOrNull(
+            codebase: Codebase,
+            literal: String
+        ): Pair<String, FieldItem?>? {
+            if (literal.contains('/')) {
+                return null
+            }
+            val parts = literal.split('.')
+
+            val flag = parts.lastOrNull() ?: return null
+            val flagField = "FLAG_" + flag.toUpperCaseAsciiOnly()
+            val pkg = parts.dropLast(1).joinToString(separator = ".")
+            val className = "$pkg.Flags"
+            val fieldSource = "$className.$flagField"
+
+            val clazzOrNull = codebase.findClass(className)
+            val fieldOrNull =
+                clazzOrNull?.findField(
+                    flagField,
+                    includeSuperClasses = true,
+                    includeInterfaces = true
+                )
+            return fieldSource to fieldOrNull
+        }
     }
 }
 
diff --git a/metalava/src/main/java/com/android/tools/metalava/stub/JavaStubWriter.kt b/metalava/src/main/java/com/android/tools/metalava/stub/JavaStubWriter.kt
index 6be6520..93a34f1 100644
--- a/metalava/src/main/java/com/android/tools/metalava/stub/JavaStubWriter.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/stub/JavaStubWriter.kt
@@ -24,6 +24,7 @@
 import com.android.tools.metalava.model.MethodItem
 import com.android.tools.metalava.model.ModifierListWriter
 import com.android.tools.metalava.model.PrimitiveTypeItem
+import com.android.tools.metalava.model.ThrowableType
 import com.android.tools.metalava.model.TypeParameterList
 import com.android.tools.metalava.model.VariableTypeItem
 import java.io.PrintWriter
@@ -237,7 +238,7 @@
                                     constructor
                                         .containingClass()
                                         .mapTypeVariables(it.containingClass())
-                                val cast = map[type]?.toTypeString() ?: typeString
+                                val cast = map[type.asTypeParameter]?.toTypeString() ?: typeString
                                 writer.write(cast)
                             } else {
                                 writer.write(typeString)
@@ -384,7 +385,7 @@
             }
         if (throws.any()) {
             writer.print(" throws ")
-            throws.sortedWith(ClassItem.fullNameComparator).forEachIndexed { i, type ->
+            throws.sortedWith(ThrowableType.fullNameComparator).forEachIndexed { i, type ->
                 if (i > 0) {
                     writer.print(", ")
                 }
diff --git a/metalava/src/main/java/com/android/tools/metalava/stub/KotlinStubWriter.kt b/metalava/src/main/java/com/android/tools/metalava/stub/KotlinStubWriter.kt
index 64dbf4c..7d7d19e 100644
--- a/metalava/src/main/java/com/android/tools/metalava/stub/KotlinStubWriter.kt
+++ b/metalava/src/main/java/com/android/tools/metalava/stub/KotlinStubWriter.kt
@@ -21,6 +21,7 @@
 import com.android.tools.metalava.model.Item
 import com.android.tools.metalava.model.MethodItem
 import com.android.tools.metalava.model.ModifierListWriter
+import com.android.tools.metalava.model.ThrowableType
 import com.android.tools.metalava.model.TypeItem
 import com.android.tools.metalava.model.TypeParameterList
 import com.android.tools.metalava.model.psi.PsiClassItem
@@ -226,7 +227,9 @@
             }
         if (throws.any()) {
             writer.print("@Throws(")
-            throws.asSequence().sortedWith(ClassItem.fullNameComparator).forEachIndexed { i, type ->
+            throws.asSequence().sortedWith(ThrowableType.fullNameComparator).forEachIndexed {
+                i,
+                type ->
                 if (i > 0) {
                     writer.print(",")
                 }
diff --git a/metalava/src/test/java/com/android/tools/metalava/ApiFromTextTest.kt b/metalava/src/test/java/com/android/tools/metalava/ApiFromTextTest.kt
index 3ed54e9..46d771d 100644
--- a/metalava/src/test/java/com/android/tools/metalava/ApiFromTextTest.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/ApiFromTextTest.kt
@@ -222,7 +222,7 @@
                 package test.pkg {
                   public class MyTest {
                     method public static int codePointAt(char[], int);
-                    method @NonNull public <K,V> java.util.Set<java.util.Map.Entry<K,V>> entrySet();
+                    method @NonNull public <K, V> java.util.Set<java.util.Map.Entry<K,V>> entrySet();
                     method @NonNull public java.lang.annotation.Annotation[] getAnnotations();
                     method @NonNull public abstract java.lang.annotation.Annotation[][] getParameterAnnotations();
                     method @NonNull public String[] split(@NonNull String, int);
@@ -308,7 +308,7 @@
         val source =
             """
             package a.b.c {
-              public interface MyStream<T, S extends a.b.c.MyStream<T, S>> {
+              public interface MyStream<T, S extends a.b.c.MyStream<T,S>> {
               }
             }
             package test.pkg {
@@ -335,7 +335,7 @@
               public final class Test<T> {
                 ctor public Test();
                 method public abstract <T extends java.util.Collection<java.lang.String>> T addAllTo(T);
-                method public static <T & java.lang.Comparable<? super T>> T max(java.util.Collection<? extends T>);
+                method public static <T extends java.lang.Object & java.lang.Comparable<? super T>> T max(java.util.Collection<? extends T>);
                 method public <X extends java.lang.Throwable> T orElseThrow(java.util.function.Supplier<? extends X>) throws java.lang.Throwable;
                 field public static java.util.List<java.lang.String> LIST;
               }
diff --git a/metalava/src/test/java/com/android/tools/metalava/ComparisonVisitorTest.kt b/metalava/src/test/java/com/android/tools/metalava/ComparisonVisitorTest.kt
index a2383ec..cffb829 100644
--- a/metalava/src/test/java/com/android/tools/metalava/ComparisonVisitorTest.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/ComparisonVisitorTest.kt
@@ -80,7 +80,7 @@
             .compare(
                 object : ComparisonVisitor() {
                     override fun added(new: MethodItem) {
-                        methodType = new.type()?.toSimpleType()
+                        methodType = new.type().toSimpleType()
                     }
                 },
                 old,
diff --git a/metalava/src/test/java/com/android/tools/metalava/DefaultReporterTest.kt b/metalava/src/test/java/com/android/tools/metalava/DefaultReporterTest.kt
index fd075d1..7961d01 100644
--- a/metalava/src/test/java/com/android/tools/metalava/DefaultReporterTest.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/DefaultReporterTest.kt
@@ -144,7 +144,7 @@
                     package test.pkg;
 
                     public class Foo {
-                        public void foo1(String a) {} 
+                        public void foo1(String a) {}
                     }
                 """
                     ),
@@ -184,11 +184,11 @@
                     package test.pkg;
 
                     public class Foo {
-                        public void foo1(String a) {} 
-                        public void foo2(String a) {} 
-                        public void foo3(String a) {} 
-                        public void foo4(String a) {} 
-                        public void foo5(String a) {} 
+                        public void foo1(String a) {}
+                        public void foo2(String a) {}
+                        public void foo3(String a) {}
+                        public void foo4(String a) {}
+                        public void foo5(String a) {}
                     }
                 """
                     ),
@@ -230,12 +230,12 @@
                     package test.pkg;
 
                     public class Foo {
-                        public void foo1(String a) {} 
-                        public void foo2(String a) {} 
-                        public void foo3(String a) {} 
-                        public void foo4(String a) {} 
-                        public void foo5(String a) {} 
-                        public void foo6(String a) {} 
+                        public void foo1(String a) {}
+                        public void foo2(String a) {}
+                        public void foo3(String a) {}
+                        public void foo4(String a) {}
+                        public void foo5(String a) {}
+                        public void foo6(String a) {}
                     }
                 """
                     ),
diff --git a/metalava/src/test/java/com/android/tools/metalava/DriverTest.kt b/metalava/src/test/java/com/android/tools/metalava/DriverTest.kt
index 141f779..959dc94 100644
--- a/metalava/src/test/java/com/android/tools/metalava/DriverTest.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/DriverTest.kt
@@ -42,7 +42,7 @@
 import com.android.tools.metalava.cli.compatibility.ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED
 import com.android.tools.metalava.cli.compatibility.ARG_ERROR_MESSAGE_CHECK_COMPATIBILITY_RELEASED
 import com.android.tools.metalava.cli.signature.ARG_FORMAT
-import com.android.tools.metalava.model.psi.gatherSources
+import com.android.tools.metalava.model.source.SourceSet
 import com.android.tools.metalava.model.text.ApiClassResolution
 import com.android.tools.metalava.model.text.ApiFile
 import com.android.tools.metalava.model.text.FileFormat
@@ -393,10 +393,32 @@
         @Language("TEXT") signatureSource: String? = null,
         /** An optional API jar file content to load **instead** of Java/Kotlin source files */
         apiJar: File? = null,
-        /** An optional API signature to check the last released API's compatibility with */
+        /**
+         * An optional API signature to check the last released API's compatibility with.
+         *
+         * This can either be the name of a file or the contents of the signature file. In the
+         * latter case the contents are adjusted to make sure it is a valid signature file with a
+         * valid header and written to a file.
+         */
         @Language("TEXT") checkCompatibilityApiReleased: String? = null,
-        /** An optional API signature to check the last released removed API's compatibility with */
+        /**
+         * Allow specifying multiple instances of [checkCompatibilityApiReleased].
+         *
+         * In order from narrowest to widest API.
+         */
+        checkCompatibilityApiReleasedList: List<String> = emptyList(),
+        /**
+         * An optional API signature to check the last released removed API's compatibility with.
+         *
+         * See [checkCompatibilityApiReleased].
+         */
         @Language("TEXT") checkCompatibilityRemovedApiReleased: String? = null,
+        /**
+         * Allow specifying multiple instances of [checkCompatibilityRemovedApiReleased].
+         *
+         * In order from narrowest to widest API.
+         */
+        checkCompatibilityRemovedApiReleasedList: List<String> = emptyList(),
         /** An optional API signature to use as the base API codebase during compat checks */
         @Language("TEXT") checkCompatibilityBaseApi: String? = null,
         @Language("TEXT") migrateNullsApi: String? = null,
@@ -487,6 +509,8 @@
         @Language("TEXT") apiLint: String? = null,
         /** The source files to pass to the analyzer */
         sourceFiles: Array<TestFile> = emptyArray(),
+        /** The common source files to pass to the analyzer */
+        commonSourceFiles: Array<TestFile> = emptyArray(),
         /** [ARG_REPEAT_ERRORS_MAX] */
         repeatErrorsMax: Int = 0
     ) {
@@ -502,13 +526,26 @@
         // Ensure that lint infrastructure (for UAST) knows it's dealing with a test
         LintCliClient(LintClient.CLIENT_UNIT_TESTS)
 
+        val releasedApiCheck =
+            CompatibilityCheckRequest.create(
+                optionName = ARG_CHECK_COMPATIBILITY_API_RELEASED,
+                fileOrSignatureContents = checkCompatibilityApiReleased,
+                fileOrSignatureContentsList = checkCompatibilityApiReleasedList,
+                newBasename = "released-api.txt",
+            )
+        val releasedRemovedApiCheck =
+            CompatibilityCheckRequest.create(
+                optionName = ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED,
+                fileOrSignatureContents = checkCompatibilityRemovedApiReleased,
+                fileOrSignatureContentsList = checkCompatibilityRemovedApiReleasedList,
+                newBasename = "removed-released-api.txt",
+            )
+
         val actualExpectedFail =
             when {
                 expectedFail != null -> expectedFail
-                (checkCompatibilityApiReleased != null ||
-                    checkCompatibilityRemovedApiReleased != null) &&
-                    expectedIssues != null &&
-                    expectedIssues.trim().isNotEmpty() -> {
+                (releasedApiCheck.required() || releasedRemovedApiCheck.required()) &&
+                    !expectedIssues.isNullOrBlank() -> {
                     "Aborting: Found compatibility problems"
                 }
                 else -> ""
@@ -517,7 +554,7 @@
         // Unit test which checks that a signature file is as expected
         val androidJar = getAndroidJar()
 
-        val project = createProject(sourceFiles)
+        val project = createProject(sourceFiles + commonSourceFiles)
 
         val sourcePathDir = File(project, "src")
         if (!sourcePathDir.isDirectory) {
@@ -525,12 +562,25 @@
         }
 
         var sourcePath = sourcePathDir.path
+        var commonSourcePath: String? = null
 
         // Make it easy to configure a source path with more than one source root: src and src2
         if (sourceFiles.any { it.targetPath.startsWith("src2") }) {
             sourcePath = sourcePath + File.pathSeparator + sourcePath + "2"
         }
 
+        fun pathUnderProject(path: String): String = File(project, path).path
+
+        if (commonSourceFiles.isNotEmpty()) {
+            // Assume common/source are placed in different folders, e.g., commonMain, androidMain
+            sourcePath =
+                pathUnderProject(sourceFiles.first().targetPath.substringBefore("src") + "src")
+            commonSourcePath =
+                pathUnderProject(
+                    commonSourceFiles.first().targetPath.substringBefore("src") + "src"
+                )
+        }
+
         val apiClassResolutionArgs =
             arrayOf(ARG_API_CLASS_RESOLUTION, apiClassResolution.optionValue)
 
@@ -562,9 +612,9 @@
                 }
                 arrayOf(apiJar.path)
             } else {
-                sourceFiles
+                (sourceFiles + commonSourceFiles)
                     .asSequence()
-                    .map { File(project, it.targetPath).path }
+                    .map { pathUnderProject(it.targetPath) }
                     .toList()
                     .toTypedArray()
             }
@@ -659,20 +709,6 @@
                 emptyArray()
             }
 
-        val checkCompatibilityApiReleasedFile =
-            useExistingSignatureFileOrCreateNewFile(
-                project,
-                checkCompatibilityApiReleased,
-                "released-api.txt"
-            )
-
-        val checkCompatibilityRemovedApiReleasedFile =
-            useExistingSignatureFileOrCreateNewFile(
-                project,
-                checkCompatibilityRemovedApiReleased,
-                "removed-released-api.txt"
-            )
-
         val checkCompatibilityBaseApiFile =
             useExistingSignatureFileOrCreateNewFile(
                 project,
@@ -699,16 +735,6 @@
                 emptyArray()
             }
 
-        val checkCompatibilityApiReleasedArguments =
-            if (checkCompatibilityApiReleasedFile != null) {
-                arrayOf(
-                    ARG_CHECK_COMPATIBILITY_API_RELEASED,
-                    checkCompatibilityApiReleasedFile.path
-                )
-            } else {
-                emptyArray()
-            }
-
         val checkCompatibilityBaseApiArguments =
             if (checkCompatibilityBaseApiFile != null) {
                 arrayOf(ARG_CHECK_COMPATIBILITY_BASE_API, checkCompatibilityBaseApiFile.path)
@@ -716,16 +742,6 @@
                 emptyArray()
             }
 
-        val checkCompatibilityRemovedReleasedArguments =
-            if (checkCompatibilityRemovedApiReleasedFile != null) {
-                arrayOf(
-                    ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED,
-                    checkCompatibilityRemovedApiReleasedFile.path
-                )
-            } else {
-                emptyArray()
-            }
-
         val quiet =
             if (expectedOutput != null && !extraArguments.contains(ARG_VERBOSE)) {
                 // If comparing output, avoid noisy output such as the banner etc
@@ -1016,9 +1032,9 @@
                 *javaStubAnnotationsArgs,
                 *inclusionAnnotationsArgs,
                 *migrateNullsArguments,
-                *checkCompatibilityApiReleasedArguments,
+                *releasedApiCheck.arguments(project),
                 *checkCompatibilityBaseApiArguments,
-                *checkCompatibilityRemovedReleasedArguments,
+                *releasedRemovedApiCheck.arguments(project),
                 *proguardKeepArguments,
                 *manifestFileArgs,
                 *applyApiLevelsXmlArgs,
@@ -1044,7 +1060,14 @@
                 *errorMessageApiLintArgs,
                 *errorMessageCheckCompatibilityReleasedArgs,
                 *repeatErrorsMaxArgs,
-            )
+            ) +
+                buildList {
+                        if (commonSourcePath != null) {
+                            add(ARG_COMMON_SOURCE_PATH)
+                            add(commonSourcePath)
+                        }
+                    }
+                    .toTypedArray()
 
         val actualOutput =
             runDriver(
@@ -1222,7 +1245,8 @@
 
         if (checkCompilation && stubsDir != null) {
             val generated =
-                gatherSources(options.reporter, listOf(stubsDir))
+                SourceSet.createFromSourcePath(options.reporter, listOf(stubsDir))
+                    .sources
                     .asSequence()
                     .map { it.path }
                     .toList()
@@ -1239,7 +1263,8 @@
                 )
             }
             val extraAnnotations =
-                gatherSources(options.reporter, listOf(extraAnnotationsDir))
+                SourceSet.createFromSourcePath(options.reporter, listOf(extraAnnotationsDir))
+                    .sources
                     .asSequence()
                     .map { it.path }
                     .toList()
@@ -1257,31 +1282,45 @@
         }
     }
 
-    /**
-     * Get an optional signature API [File] from either a file path or its contents.
-     *
-     * @param project the directory in which to create a new file.
-     * @param fileOrFileContents either a path to an existing file or the contents of the signature
-     *   file. If the latter the contents will be trimmed, updated to add a [FileFormat.V2] header
-     *   if needed and written to a new file created within [project].
-     * @param newBasename the basename of a new file created.
-     */
-    private fun useExistingSignatureFileOrCreateNewFile(
-        project: File,
-        fileOrFileContents: String?,
-        newBasename: String
-    ) =
-        fileOrFileContents?.let {
-            val maybeFile = File(fileOrFileContents)
-            if (maybeFile.isFile) {
-                maybeFile
-            } else {
-                val file = File(project, newBasename)
-                file.writeSignatureText(fileOrFileContents)
-                file
-            }
+    /** Encapsulates information needed to request a compatibility check. */
+    private class CompatibilityCheckRequest
+    private constructor(
+        private val optionName: String,
+        private val fileOrSignatureContentsList: List<String>,
+        private val newBasename: String,
+    ) {
+        companion object {
+            fun create(
+                optionName: String,
+                fileOrSignatureContents: String?,
+                fileOrSignatureContentsList: List<String>,
+                newBasename: String,
+            ): CompatibilityCheckRequest =
+                CompatibilityCheckRequest(
+                    optionName = optionName,
+                    fileOrSignatureContentsList =
+                        listOfNotNull(fileOrSignatureContents) + fileOrSignatureContentsList,
+                    newBasename = newBasename,
+                )
         }
 
+        /** Indicates whether the compatibility check is required. */
+        fun required(): Boolean = fileOrSignatureContentsList.isNotEmpty()
+
+        /** The arguments to pass to Metalava. */
+        fun arguments(project: File): Array<out String> {
+            if (fileOrSignatureContentsList.isEmpty()) return emptyArray()
+
+            val paths =
+                fileOrSignatureContentsList.mapNotNull {
+                    useExistingSignatureFileOrCreateNewFile(project, it, newBasename)?.path
+                }
+
+            // For each path in the list generate an option with the path as the value.
+            return paths.flatMap { listOf(optionName, it) }.toTypedArray()
+        }
+    }
+
     protected fun uastCheck(
         isK2: Boolean,
         @Language("TEXT") api: String? = null,
@@ -1291,6 +1330,7 @@
         expectedFail: String? = null,
         @Language("TEXT") apiLint: String? = null,
         sourceFiles: Array<TestFile> = emptyArray(),
+        commonSourceFiles: Array<TestFile> = emptyArray(),
     ) {
         check(
             api = api,
@@ -1300,6 +1340,7 @@
             expectedFail = expectedFail,
             apiLint = apiLint,
             sourceFiles = sourceFiles,
+            commonSourceFiles = commonSourceFiles,
         )
     }
 
@@ -1353,6 +1394,31 @@
             apiLines = apiLines.filter { it.isNotBlank() }
             return apiLines.joinToString(separator = "\n") { it }.trim()
         }
+
+        /**
+         * Get an optional signature API [File] from either a file path or its contents.
+         *
+         * @param project the directory in which to create a new file.
+         * @param fileOrFileContents either a path to an existing file or the contents of the
+         *   signature file. If the latter the contents will be trimmed, updated to add a
+         *   [FileFormat.V2] header if needed and written to a new file created within [project].
+         * @param newBasename the basename of a new file created.
+         */
+        private fun useExistingSignatureFileOrCreateNewFile(
+            project: File,
+            fileOrFileContents: String?,
+            newBasename: String
+        ) =
+            fileOrFileContents?.let {
+                val maybeFile = File(fileOrFileContents)
+                if (maybeFile.isFile) {
+                    maybeFile
+                } else {
+                    val file = File(project, newBasename)
+                    file.writeSignatureText(fileOrFileContents)
+                    file
+                }
+            }
     }
 }
 
@@ -1628,22 +1694,7 @@
         )
         .indented()
 
-val supportParameterName: TestFile =
-    java(
-            """
-    package androidx.annotation;
-    import java.lang.annotation.*;
-    import static java.lang.annotation.ElementType.*;
-    import static java.lang.annotation.RetentionPolicy.SOURCE;
-    @SuppressWarnings("WeakerAccess")
-    @Retention(SOURCE)
-    @Target({METHOD, PARAMETER, FIELD})
-    public @interface ParameterName {
-        String value();
-    }
-    """
-        )
-        .indented()
+val supportParameterName = KnownSourceFiles.supportParameterName
 
 val supportDefaultValue: TestFile =
     java(
diff --git a/metalava/src/test/java/com/android/tools/metalava/FlaggedApiTest.kt b/metalava/src/test/java/com/android/tools/metalava/FlaggedApiTest.kt
index 667c3cf..af2ac28 100644
--- a/metalava/src/test/java/com/android/tools/metalava/FlaggedApiTest.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/FlaggedApiTest.kt
@@ -283,7 +283,7 @@
                         Flagged.WITHOUT,
                         expectedApi =
                             """
-                                // Signature format: 2.0                        
+                                // Signature format: 2.0
                             """,
                     ),
                 ),
diff --git a/metalava/src/test/java/com/android/tools/metalava/KotlinInteropChecksTest.kt b/metalava/src/test/java/com/android/tools/metalava/KotlinInteropChecksTest.kt
index ce5a2bf..dea47c0 100644
--- a/metalava/src/test/java/com/android/tools/metalava/KotlinInteropChecksTest.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/KotlinInteropChecksTest.kt
@@ -201,7 +201,7 @@
                     interface Bar {
                         fun ok(int: Int = 0, int2: Int = 0) { }
                     }
-                    
+
                     class Foo {
                         fun ok1() { }
                         fun ok2(int: Int) { }
diff --git a/metalava/src/test/java/com/android/tools/metalava/MainCommandTest.kt b/metalava/src/test/java/com/android/tools/metalava/MainCommandTest.kt
index 0280110..0d8f40e 100644
--- a/metalava/src/test/java/com/android/tools/metalava/MainCommandTest.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/MainCommandTest.kt
@@ -103,6 +103,10 @@
 --source-path <paths>
                                              One or more directories (separated by `:`) containing source files (within
                                              a package hierarchy).
+--common-source-path <paths>
+                                             One or more directories (separated by `:`) containing common source files
+                                             (within a package hierarchy) where platform-agnostic `expect` declarations
+                                             as well as common business logic are defined.
 --classpath <paths>
                                              One or more directories or jars (separated by `:`) containing classes that
                                              should be on the classpath when parsing the source files
diff --git a/metalava/src/test/java/com/android/tools/metalava/SignatureInputOutputTest.kt b/metalava/src/test/java/com/android/tools/metalava/SignatureInputOutputTest.kt
index c62a1e5..8e726ef 100644
--- a/metalava/src/test/java/com/android/tools/metalava/SignatureInputOutputTest.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/SignatureInputOutputTest.kt
@@ -17,21 +17,23 @@
 package com.android.tools.metalava
 
 import com.android.tools.metalava.model.ArrayTypeItem
+import com.android.tools.metalava.model.Assertions
 import com.android.tools.metalava.model.ClassTypeItem
 import com.android.tools.metalava.model.Codebase
 import com.android.tools.metalava.model.PrimitiveTypeItem
 import com.android.tools.metalava.model.VisibilityLevel
 import com.android.tools.metalava.model.text.ApiFile
 import com.android.tools.metalava.model.text.FileFormat
-import com.android.tools.metalava.model.text.TextMethodItem
 import com.android.tools.metalava.model.text.assertSignatureFilesMatch
 import com.android.tools.metalava.model.visitors.ApiVisitor
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
 import java.io.StringWriter
+import org.junit.Assert.assertThrows
+import org.junit.ComparisonFailure
 import org.junit.Test
 
-class SignatureInputOutputTest {
+class SignatureInputOutputTest : Assertions {
     /**
      * Parses the API (without a header line, the header from [format] will be added) from the
      * [signature], runs the [codebaseTest] on the parsed codebase, and then writes the codebase
@@ -84,9 +86,8 @@
                 .trimIndent()
 
         runInputOutputTest(api, kotlinStyleFormat) { codebase ->
-            val foo = codebase.findClass("test.pkg.Foo")
-            assertThat(foo).isNotNull()
-            assertThat(foo!!.constructors()).hasSize(1)
+            val foo = codebase.assertClass("test.pkg.Foo")
+            assertThat(foo.constructors()).hasSize(1)
             val ctor = foo.constructors().single()
             assertThat(ctor.parameters()).isEmpty()
         }
@@ -104,9 +105,8 @@
             """
                 .trimIndent()
         runInputOutputTest(api, kotlinStyleFormat) { codebase ->
-            val foo = codebase.findClass("test.pkg.Foo")
-            assertThat(foo).isNotNull()
-            assertThat(foo!!.properties()).hasSize(1)
+            val foo = codebase.assertClass("test.pkg.Foo")
+            assertThat(foo.properties()).hasSize(1)
 
             val prop = foo.properties().single()
             assertThat(prop.name()).isEqualTo("foo")
@@ -127,9 +127,8 @@
             """
                 .trimIndent()
         runInputOutputTest(api, kotlinStyleFormat) { codebase ->
-            val foo = codebase.findClass("test.pkg.Foo")
-            assertThat(foo).isNotNull()
-            assertThat(foo!!.fields()).hasSize(1)
+            val foo = codebase.assertClass("test.pkg.Foo")
+            assertThat(foo.fields()).hasSize(1)
 
             val field = foo.fields().single()
             assertThat(field.name()).isEqualTo("foo")
@@ -151,9 +150,8 @@
             """
                 .trimIndent()
         runInputOutputTest(api, kotlinStyleFormat) { codebase ->
-            val foo = codebase.findClass("test.pkg.Foo")
-            assertThat(foo).isNotNull()
-            assertThat(foo!!.fields()).hasSize(1)
+            val foo = codebase.assertClass("test.pkg.Foo")
+            assertThat(foo.fields()).hasSize(1)
 
             val field = foo.fields().single()
             assertThat(field.name()).isEqualTo("foo")
@@ -176,9 +174,8 @@
             """
                 .trimIndent()
         runInputOutputTest(api, kotlinStyleFormat) { codebase ->
-            val foo = codebase.findClass("test.pkg.Foo")
-            assertThat(foo).isNotNull()
-            assertThat(foo!!.methods()).hasSize(1)
+            val foo = codebase.assertClass("test.pkg.Foo")
+            assertThat(foo.methods()).hasSize(1)
 
             val method = foo.methods().single()
             assertThat(method.name()).isEqualTo("foo")
@@ -200,9 +197,8 @@
             """
                 .trimIndent()
         runInputOutputTest(api, kotlinStyleFormat) { codebase ->
-            val foo = codebase.findClass("test.pkg.Foo")
-            assertThat(foo).isNotNull()
-            assertThat(foo!!.methods()).hasSize(1)
+            val foo = codebase.assertClass("test.pkg.Foo")
+            assertThat(foo.methods()).hasSize(1)
 
             val method = foo.methods().single()
             assertThat(method.name()).isEqualTo("foo")
@@ -229,9 +225,8 @@
             """
                 .trimIndent()
         runInputOutputTest(api, kotlinStyleFormat) { codebase ->
-            val foo = codebase.findClass("test.pkg.Foo")
-            assertThat(foo).isNotNull()
-            assertThat(foo!!.methods()).hasSize(1)
+            val foo = codebase.assertClass("test.pkg.Foo")
+            assertThat(foo.methods()).hasSize(1)
 
             val method = foo.methods().single()
             assertThat(method.name()).isEqualTo("foo")
@@ -257,9 +252,8 @@
             """
                 .trimIndent()
         runInputOutputTest(api, kotlinStyleFormat) { codebase ->
-            val foo = codebase.findClass("test.pkg.Foo")
-            assertThat(foo).isNotNull()
-            val method = foo!!.methods().single()
+            val foo = codebase.assertClass("test.pkg.Foo")
+            val method = foo.methods().single()
 
             assertThat(method.parameters()).hasSize(1)
             val param = method.parameters().single()
@@ -282,9 +276,8 @@
             """
                 .trimIndent()
         runInputOutputTest(api, kotlinStyleFormat) { codebase ->
-            val foo = codebase.findClass("test.pkg.Foo")
-            assertThat(foo).isNotNull()
-            val method = foo!!.methods().single()
+            val foo = codebase.assertClass("test.pkg.Foo")
+            val method = foo.methods().single()
 
             assertThat(method.parameters()).hasSize(1)
             val param = method.parameters().single()
@@ -311,9 +304,8 @@
             """
                 .trimIndent()
         runInputOutputTest(api, format) { codebase ->
-            val foo = codebase.findClass("test.pkg.Foo")
-            assertThat(foo).isNotNull()
-            val method = foo!!.methods().single()
+            val foo = codebase.assertClass("test.pkg.Foo")
+            val method = foo.methods().single()
 
             assertThat(method.parameters()).hasSize(1)
             val param = method.parameters().single()
@@ -340,9 +332,8 @@
             """
                 .trimIndent()
         runInputOutputTest(api, kotlinStyleFormat) { codebase ->
-            val foo = codebase.findClass("test.pkg.Foo")
-            assertThat(foo).isNotNull()
-            val method = foo!!.methods().single()
+            val foo = codebase.assertClass("test.pkg.Foo")
+            val method = foo.methods().single()
 
             assertThat(method.parameters()).hasSize(1)
             val param = method.parameters().single()
@@ -352,7 +343,6 @@
             assertThat((param.type() as ArrayTypeItem).isVarargs).isTrue()
             assertThat(param.isVarArgs()).isTrue()
             assertThat(param.modifiers.isVarArg()).isTrue()
-            assertThat((method as TextMethodItem).isVarArg()).isTrue()
         }
     }
 
@@ -369,9 +359,8 @@
             """
                     .trimIndent()
         runInputOutputTest(api, kotlinStyleFormat) { codebase ->
-            val foo = codebase.findClass("test.pkg.Foo")
-            assertThat(foo).isNotNull()
-            val method = foo!!.methods().single()
+            val foo = codebase.assertClass("test.pkg.Foo")
+            val method = foo.methods().single()
 
             assertThat(method.parameters()).hasSize(1)
             val param = method.parameters().single()
@@ -394,9 +383,8 @@
             """
                 .trimIndent()
         runInputOutputTest(api, kotlinStyleFormat) { codebase ->
-            val foo = codebase.findClass("test.pkg.Foo")
-            assertThat(foo).isNotNull()
-            val method = foo!!.methods().single()
+            val foo = codebase.assertClass("test.pkg.Foo")
+            val method = foo.methods().single()
 
             assertThat(method.parameters()).hasSize(1)
             val param = method.parameters().single()
@@ -419,9 +407,8 @@
             """
                 .trimIndent()
         runInputOutputTest(api, kotlinStyleFormat) { codebase ->
-            val foo = codebase.findClass("test.pkg.Foo")
-            assertThat(foo).isNotNull()
-            val method = foo!!.methods().single()
+            val foo = codebase.assertClass("test.pkg.Foo")
+            val method = foo.methods().single()
 
             assertThat(method.parameters()).hasSize(3)
 
@@ -438,9 +425,9 @@
             assertThat(p1.publicName()).isEqualTo("map")
             val mapType = p1.type() as ClassTypeItem
             assertThat(mapType.qualifiedName).isEqualTo("java.util.Map")
-            assertThat(mapType.parameters).hasSize(2)
-            assertThat(mapType.parameters[0].isString()).isTrue()
-            assertThat(mapType.parameters[1].isJavaLangObject()).isTrue()
+            assertThat(mapType.arguments).hasSize(2)
+            assertThat(mapType.arguments[0].isString()).isTrue()
+            assertThat(mapType.arguments[1].isJavaLangObject()).isTrue()
 
             // arr: String[]
             val p2 = method.parameters()[2]
@@ -462,9 +449,8 @@
             """
                 .trimIndent()
         runInputOutputTest(api, kotlinStyleFormat) { codebase ->
-            val foo = codebase.findClass("test.pkg.Foo")
-            assertThat(foo).isNotNull()
-            val method = foo!!.methods().single()
+            val foo = codebase.assertClass("test.pkg.Foo")
+            val method = foo.methods().single()
 
             assertThat(method.parameters()).hasSize(3)
 
@@ -481,9 +467,9 @@
             assertThat(p1.publicName()).isNull()
             val mapType = p1.type() as ClassTypeItem
             assertThat(mapType.qualifiedName).isEqualTo("java.util.Map")
-            assertThat(mapType.parameters).hasSize(2)
-            assertThat(mapType.parameters[0].isString()).isTrue()
-            assertThat(mapType.parameters[1].isJavaLangObject()).isTrue()
+            assertThat(mapType.arguments).hasSize(2)
+            assertThat(mapType.arguments[0].isString()).isTrue()
+            assertThat(mapType.arguments[1].isJavaLangObject()).isTrue()
 
             // _: String[]
             val p2 = method.parameters()[2]
@@ -506,7 +492,7 @@
             """
                 .trimIndent()
         runInputOutputTest(api, format) { codebase ->
-            val method = codebase.findClass("test.pkg.MyTest")!!.methods().single()
+            val method = codebase.assertClass("test.pkg.MyTest").methods().single()
             // Return type has platform nullability
             assertThat(method.hasNullnessInfo()).isFalse()
 
@@ -543,7 +529,7 @@
             """
                 .trimIndent()
         runInputOutputTest(api, format) { codebase ->
-            val fooClass = codebase.findClass("test.pkg.Foo")!!
+            val fooClass = codebase.assertClass("test.pkg.Foo")
             val superClassType = fooClass.superClassType()
             assertThat(superClassType!!.modifiers.annotations().map { it.qualifiedName })
                 .containsExactly("test.pkg.A")
@@ -553,6 +539,66 @@
         }
     }
 
+    @Test
+    fun `Test generic super class with nullable type`() {
+        val api =
+            """
+                package test.pkg {
+                  public interface Foo extends kotlin.collections.List<java.lang.String?> {
+                  }
+                }
+            """
+                .trimIndent()
+        val exception =
+            assertThrows(ComparisonFailure::class.java) {
+                runInputOutputTest(api, kotlinStyleFormat) {}
+            }
+
+        // Note that the List type argument is "String". not "String?" as it is above.
+        assertThat(exception.actual)
+            .isEqualTo(
+                """
+                // Signature format: 5.0
+                // - kotlin-name-type-order=yes
+                package test.pkg {
+                  public interface Foo extends kotlin.collections.List<java.lang.String> {
+                  }
+                }
+            """
+                    .trimIndent()
+            )
+    }
+
+    @Test
+    fun `Test generic super interface with nullable type`() {
+        val api =
+            """
+                package test.pkg {
+                  public class Foo implements kotlin.collections.List<java.lang.String?> {
+                  }
+                }
+            """
+                .trimIndent()
+        val exception =
+            assertThrows(ComparisonFailure::class.java) {
+                runInputOutputTest(api, kotlinStyleFormat) {}
+            }
+
+        // Note that the List type argument is "String". not "String?" as it is above.
+        assertThat(exception.actual)
+            .isEqualTo(
+                """
+                // Signature format: 5.0
+                // - kotlin-name-type-order=yes
+                package test.pkg {
+                  public class Foo implements kotlin.collections.List<java.lang.String> {
+                  }
+                }
+            """
+                    .trimIndent()
+            )
+    }
+
     companion object {
         private val kotlinStyleFormat =
             FileFormat.V5.copy(kotlinNameTypeOrder = true, formatDefaults = FileFormat.V5)
diff --git a/metalava/src/test/java/com/android/tools/metalava/UastTestBase.kt b/metalava/src/test/java/com/android/tools/metalava/UastTestBase.kt
index da3ac09..8b700fe 100644
--- a/metalava/src/test/java/com/android/tools/metalava/UastTestBase.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/UastTestBase.kt
@@ -193,8 +193,7 @@
     }
 
     protected fun `Annotation on parameters of data class synthetic copy`(isK2: Boolean) {
-        // TODO: https://youtrack.jetbrains.com/issue/KT-57003
-        val typeAnno = if (isK2) "" else "@test.pkg.MyAnnotation "
+        // https://youtrack.jetbrains.com/issue/KT-57003
         uastCheck(
             isK2,
             sourceFiles =
@@ -215,7 +214,7 @@
                     ctor public Foo(@test.pkg.MyAnnotation int p1, String p2);
                     method public int component1();
                     method public String component2();
-                    method public test.pkg.Foo copy(${typeAnno}int p1, String p2);
+                    method public test.pkg.Foo copy(@test.pkg.MyAnnotation int p1, String p2);
                     method public int getP1();
                     method public String getP2();
                     property public final int p1;
@@ -983,28 +982,28 @@
                         package test.pkg
 
                         class Test_noAccessor {
-                            @Deprecated(level = DeprecationLevel.HIDDEN, "no more property")
+                            @Deprecated("no more property", level = DeprecationLevel.HIDDEN)
                             var pOld_noAccessor_deprecatedOnProperty: String = "42"
 
-                            @get:Deprecated(level = DeprecationLevel.HIDDEN, "no more getter")
+                            @get:Deprecated("no more getter", level = DeprecationLevel.HIDDEN)
                             var pOld_noAccessor_deprecatedOnGetter: String = "42"
 
-                            @set:Deprecated(level = DeprecationLevel.HIDDEN, "no more setter")
+                            @set:Deprecated("no more setter", level = DeprecationLevel.HIDDEN)
                             var pOld_noAccessor_deprecatedOnSetter: String = "42"
 
                             var pNew_noAccessor: String = "42"
                         }
 
                         class Test_getter {
-                            @Deprecated(level = DeprecationLevel.HIDDEN, "no more property")
+                            @Deprecated("no more property", level = DeprecationLevel.HIDDEN)
                             var pOld_getter_deprecatedOnProperty: String? = null
                                 get() = field ?: "null?"
 
-                            @get:Deprecated(level = DeprecationLevel.HIDDEN, "no more getter")
+                            @get:Deprecated("no more getter", level = DeprecationLevel.HIDDEN)
                             var pOld_getter_deprecatedOnGetter: String? = null
                                 get() = field ?: "null?"
 
-                            @set:Deprecated(level = DeprecationLevel.HIDDEN, "no more setter")
+                            @set:Deprecated("no more setter", level = DeprecationLevel.HIDDEN)
                             var pOld_getter_deprecatedOnSetter: String? = null
                                 get() = field ?: "null?"
 
@@ -1013,7 +1012,7 @@
                         }
 
                         class Test_setter {
-                            @Deprecated(level = DeprecationLevel.HIDDEN, "no more property")
+                            @Deprecated("no more property", level = DeprecationLevel.HIDDEN)
                             var pOld_setter_deprecatedOnProperty: String? = null
                                 set(value) {
                                     if (field == null) {
@@ -1021,7 +1020,7 @@
                                     }
                                 }
 
-                            @get:Deprecated(level = DeprecationLevel.HIDDEN, "no more getter")
+                            @get:Deprecated("no more getter", level = DeprecationLevel.HIDDEN)
                             var pOld_setter_deprecatedOnGetter: String? = null
                                 set(value) {
                                     if (field == null) {
@@ -1029,7 +1028,7 @@
                                     }
                                 }
 
-                            @set:Deprecated(level = DeprecationLevel.HIDDEN, "no more setter")
+                            @set:Deprecated("no more setter", level = DeprecationLevel.HIDDEN)
                             var pOld_setter_deprecatedOnSetter: String? = null
                                 set(value) {
                                     if (field == null) {
@@ -1046,7 +1045,7 @@
                         }
 
                         class Test_accessors {
-                            @Deprecated(level = DeprecationLevel.HIDDEN, "no more property")
+                            @Deprecated("no more property", level = DeprecationLevel.HIDDEN)
                             var pOld_accessors_deprecatedOnProperty: String? = null
                                 get() = field ?: "null?"
                                 set(value) {
@@ -1055,7 +1054,7 @@
                                     }
                                 }
 
-                            @get:Deprecated(level = DeprecationLevel.HIDDEN, "no more getter")
+                            @get:Deprecated("no more getter", level = DeprecationLevel.HIDDEN)
                             var pOld_accessors_deprecatedOnGetter: String? = null
                                 get() = field ?: "null?"
                                 set(value) {
@@ -1064,7 +1063,7 @@
                                     }
                                 }
 
-                            @set:Deprecated(level = DeprecationLevel.HIDDEN, "no more setter")
+                            @set:Deprecated("no more setter", level = DeprecationLevel.HIDDEN)
                             var pOld_accessors_deprecatedOnSetter: String? = null
                                 get() = field ?: "null?"
                                 set(value) {
@@ -1090,52 +1089,52 @@
                         annotation class MyAnnotation
 
                         interface TestInterface {
-                            @Deprecated(level = DeprecationLevel.HIDDEN, "no more property")
+                            @Deprecated("no more property", level = DeprecationLevel.HIDDEN)
                             var pOld_deprecatedOnProperty: Int
 
                             @get:MyAnnotation
-                            @Deprecated(level = DeprecationLevel.HIDDEN, "no more property")
+                            @Deprecated("no more property", level = DeprecationLevel.HIDDEN)
                             var pOld_deprecatedOnProperty_myAnnoOnGetter: Int
 
                             @set:MyAnnotation
-                            @Deprecated(level = DeprecationLevel.HIDDEN, "no more property")
+                            @Deprecated("no more property", level = DeprecationLevel.HIDDEN)
                             var pOld_deprecatedOnProperty_myAnnoOnSetter: Int
 
                             @get:MyAnnotation
                             @set:MyAnnotation
-                            @Deprecated(level = DeprecationLevel.HIDDEN, "no more property")
+                            @Deprecated("no more property", level = DeprecationLevel.HIDDEN)
                             var pOld_deprecatedOnProperty_myAnnoOnBoth: Int
 
-                            @get:Deprecated(level = DeprecationLevel.HIDDEN, "no more getter")
+                            @get:Deprecated("no more getter", level = DeprecationLevel.HIDDEN)
                             var pOld_deprecatedOnGetter: Int
 
                             @get:MyAnnotation
-                            @get:Deprecated(level = DeprecationLevel.HIDDEN, "no more getter")
+                            @get:Deprecated("no more getter", level = DeprecationLevel.HIDDEN)
                             var pOld_deprecatedOnGetter_myAnnoOnGetter: Int
 
                             @set:MyAnnotation
-                            @get:Deprecated(level = DeprecationLevel.HIDDEN, "no more getter")
+                            @get:Deprecated("no more getter", level = DeprecationLevel.HIDDEN)
                             var pOld_deprecatedOnGetter_myAnnoOnSetter: Int
 
                             @get:MyAnnotation
                             @set:MyAnnotation
-                            @get:Deprecated(level = DeprecationLevel.HIDDEN, "no more getter")
+                            @get:Deprecated("no more getter", level = DeprecationLevel.HIDDEN)
                             var pOld_deprecatedOnGetter_myAnnoOnBoth: Int
 
-                            @set:Deprecated(level = DeprecationLevel.HIDDEN, "no more setter")
+                            @set:Deprecated("no more setter", level = DeprecationLevel.HIDDEN)
                             var pOld_deprecatedOnSetter: Int
 
                             @get:MyAnnotation
-                            @set:Deprecated(level = DeprecationLevel.HIDDEN, "no more setter")
+                            @set:Deprecated("no more setter", level = DeprecationLevel.HIDDEN)
                             var pOld_deprecatedOnSetter_myAnnoOnGetter: Int
 
                             @set:MyAnnotation
-                            @set:Deprecated(level = DeprecationLevel.HIDDEN, "no more setter")
+                            @set:Deprecated("no more setter", level = DeprecationLevel.HIDDEN)
                             var pOld_deprecatedOnSetter_myAnnoOnSetter: Int
 
                             @get:MyAnnotation
                             @set:MyAnnotation
-                            @set:Deprecated(level = DeprecationLevel.HIDDEN, "no more setter")
+                            @set:Deprecated("no more setter", level = DeprecationLevel.HIDDEN)
                             var pOld_deprecatedOnSetter_myAnnoOnBoth: Int
                         }
                         """
@@ -1144,4 +1143,163 @@
             api = api,
         )
     }
+
+    protected fun `actual typealias -- without value class`(isK2: Boolean) {
+        // https://youtrack.jetbrains.com/issue/KT-55085
+        val typeAliasExpanded = if (isK2) "test.pkg.NativePointerKeyboardModifiers" else "int"
+        val commonSource =
+            kotlin(
+                "commonMain/src/test/pkg/PointerEvent.kt",
+                """
+                        package test.pkg
+
+                        expect class PointerEvent {
+                            val keyboardModifiers: PointerKeyboardModifiers
+                        }
+
+                        expect class NativePointerKeyboardModifiers
+
+                        class PointerKeyboardModifiers(internal val packedValue: NativePointerKeyboardModifiers)
+                        """
+            )
+        uastCheck(
+            isK2,
+            sourceFiles =
+                arrayOf(
+                    kotlin(
+                        "androidMain/src/test/pkg/PointerEvent.android.kt",
+                        """
+                        package test.pkg
+
+                        actual class PointerEvent {
+                            actual val keyboardModifiers = PointerKeyboardModifiers(42)
+                        }
+
+                        internal actual typealias NativePointerKeyboardModifiers = Int
+                        """
+                    ),
+                    commonSource,
+                ),
+            commonSourceFiles = arrayOf(commonSource),
+            api =
+                """
+                package test.pkg {
+                  public final class PointerEvent {
+                    ctor public PointerEvent();
+                    method public test.pkg.PointerKeyboardModifiers getKeyboardModifiers();
+                    property public final test.pkg.PointerKeyboardModifiers keyboardModifiers;
+                  }
+                  public final class PointerKeyboardModifiers {
+                    ctor public PointerKeyboardModifiers($typeAliasExpanded packedValue);
+                  }
+                }
+                """
+        )
+    }
+
+    protected fun `actual typealias -- without common split`(isK2: Boolean) {
+        // https://youtrack.jetbrains.com/issue/KT-55085
+        val typeAliasExpanded = if (isK2) "test.pkg.NativePointerKeyboardModifiers" else "int"
+        uastCheck(
+            isK2,
+            sourceFiles =
+                arrayOf(
+                    kotlin(
+                        "androidMain/src/test/pkg/PointerEvent.android.kt",
+                        """
+                        package test.pkg
+
+                        actual class PointerEvent {
+                            actual val keyboardModifiers = PointerKeyboardModifiers(42)
+                        }
+
+                        internal actual typealias NativePointerKeyboardModifiers = Int
+                        """
+                    ),
+                    kotlin(
+                        "commonMain/src/test/pkg/PointerEvent.kt",
+                        """
+                        package test.pkg
+
+                        expect class PointerEvent {
+                            val keyboardModifiers: PointerKeyboardModifiers
+                        }
+
+                        expect class NativePointerKeyboardModifiers
+
+                        @kotlin.jvm.JvmInline
+                        value class PointerKeyboardModifiers(internal val packedValue: NativePointerKeyboardModifiers)
+                        """
+                    )
+                ),
+            api =
+                """
+                package test.pkg {
+                  public final class PointerEvent {
+                    ctor public PointerEvent();
+                    method public $typeAliasExpanded getKeyboardModifiers();
+                    property public final $typeAliasExpanded keyboardModifiers;
+                  }
+                  @kotlin.jvm.JvmInline public final value class PointerKeyboardModifiers {
+                    ctor public PointerKeyboardModifiers($typeAliasExpanded packedValue);
+                  }
+                }
+                """
+        )
+    }
+
+    protected fun `actual typealias`(isK2: Boolean) {
+        // https://youtrack.jetbrains.com/issue/KT-55085
+        // TODO: https://youtrack.jetbrains.com/issue/KTIJ-26853
+        val typeAliasExpanded = if (isK2) "test.pkg.NativePointerKeyboardModifiers" else "int"
+        val commonSource =
+            kotlin(
+                "commonMain/src/test/pkg/PointerEvent.kt",
+                """
+                        package test.pkg
+
+                        expect class PointerEvent {
+                            val keyboardModifiers: PointerKeyboardModifiers
+                        }
+
+                        expect class NativePointerKeyboardModifiers
+
+                        @kotlin.jvm.JvmInline
+                        value class PointerKeyboardModifiers(internal val packedValue: NativePointerKeyboardModifiers)
+                        """
+            )
+        uastCheck(
+            isK2,
+            sourceFiles =
+                arrayOf(
+                    kotlin(
+                        "androidMain/src/test/pkg/PointerEvent.android.kt",
+                        """
+                        package test.pkg
+
+                        actual class PointerEvent {
+                            actual val keyboardModifiers = PointerKeyboardModifiers(42)
+                        }
+
+                        internal actual typealias NativePointerKeyboardModifiers = Int
+                        """
+                    ),
+                    commonSource,
+                ),
+            commonSourceFiles = arrayOf(commonSource),
+            api =
+                """
+                package test.pkg {
+                  public final class PointerEvent {
+                    ctor public PointerEvent();
+                    method public int getKeyboardModifiers();
+                    property public final int keyboardModifiers;
+                  }
+                  @kotlin.jvm.JvmInline public final value class PointerKeyboardModifiers {
+                    ctor public PointerKeyboardModifiers($typeAliasExpanded packedValue);
+                  }
+                }
+                """
+        )
+    }
 }
diff --git a/metalava/src/test/java/com/android/tools/metalava/UastTestK1.kt b/metalava/src/test/java/com/android/tools/metalava/UastTestK1.kt
index 5225ff4..952e910 100644
--- a/metalava/src/test/java/com/android/tools/metalava/UastTestK1.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/UastTestK1.kt
@@ -217,4 +217,19 @@
             """
         )
     }
+
+    @Test
+    fun `actual typealias -- without value class -- K1`() {
+        `actual typealias -- without value class`(isK2 = false)
+    }
+
+    @Test
+    fun `actual typealias -- without common split -- K1`() {
+        `actual typealias -- without common split`(isK2 = false)
+    }
+
+    @Test
+    fun `actual typealias -- K1`() {
+        `actual typealias`(isK2 = false)
+    }
 }
diff --git a/metalava/src/test/java/com/android/tools/metalava/UastTestK2.kt b/metalava/src/test/java/com/android/tools/metalava/UastTestK2.kt
index 3325aff..b31ec5b 100644
--- a/metalava/src/test/java/com/android/tools/metalava/UastTestK2.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/UastTestK2.kt
@@ -243,4 +243,19 @@
             """
         )
     }
+
+    @Test
+    fun `actual typealias -- without value class -- K2`() {
+        `actual typealias -- without value class`(isK2 = true)
+    }
+
+    @Test
+    fun `actual typealias -- without common split -- K2`() {
+        `actual typealias -- without common split`(isK2 = true)
+    }
+
+    @Test
+    fun `actual typealias -- K2`() {
+        `actual typealias`(isK2 = true)
+    }
 }
diff --git a/metalava/src/test/java/com/android/tools/metalava/apilevels/InternalDescTest.kt b/metalava/src/test/java/com/android/tools/metalava/apilevels/InternalDescTest.kt
index 6d90d1c..01ea6d80 100644
--- a/metalava/src/test/java/com/android/tools/metalava/apilevels/InternalDescTest.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/apilevels/InternalDescTest.kt
@@ -16,12 +16,12 @@
 
 package com.android.tools.metalava.apilevels
 
+import com.android.tools.metalava.model.Assertions
 import com.android.tools.metalava.model.text.ApiFile
 import kotlin.test.assertEquals
-import kotlin.test.assertNotNull
 import org.junit.Test
 
-class InternalDescTest {
+class InternalDescTest : Assertions {
 
     @Test
     fun `MethodItem internalDesc (psi)`() {
@@ -37,8 +37,7 @@
                 }
              """
         ApiFile.parseApi("test", signature.trimIndent()).let {
-            val testClass = it.findClass("test.pkg.Test")
-            assertNotNull(testClass)
+            val testClass = it.assertClass("test.pkg.Test")
             val actual = buildString {
                 testClass.methods().forEach {
                     append(it.name()).append(it.internalDesc()).append("\n")
diff --git a/metalava/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassMethodsAndConstructors.kt b/metalava/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassMethodsAndConstructors.kt
index 3c92a0a..cfeaeb7 100644
--- a/metalava/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassMethodsAndConstructors.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassMethodsAndConstructors.kt
@@ -45,32 +45,6 @@
             """
         )
     }
-    // Note: This is reversed from the eclipse wiki because of kotlin named parameters
-    @Test
-    fun `Change formal parameter name (Incompatible)`() {
-        check(
-            expectedIssues =
-                """
-                load-api.txt:4: error: Attempted to change parameter name from bread to toast in method test.pkg.Foo.bar [ParameterNameChange]
-            """,
-            signatureSource =
-                """
-                package test.pkg {
-                  class Foo {
-                    method public void bar(Int toast);
-                  }
-                }
-            """,
-            checkCompatibilityApiReleased =
-                """
-                package test.pkg {
-                  class Foo {
-                    method public void bar(Int bread);
-                  }
-                }
-            """
-        )
-    }
 
     @Test
     fun `Add or delete formal parameter (Incompatible)`() {
diff --git a/metalava/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityInterfaceMethodsTest.kt b/metalava/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityInterfaceMethodsTest.kt
index 73ba47b..d6d2e8d 100644
--- a/metalava/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityInterfaceMethodsTest.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityInterfaceMethodsTest.kt
@@ -21,33 +21,6 @@
 
 class BinaryCompatibilityInterfaceMethodsTest : DriverTest() {
 
-    // Note: This is reversed from the eclipse wiki because of kotlin named parameters
-    @Test
-    fun `Change formal parameter name (Incompatible)`() {
-        check(
-            expectedIssues =
-                """
-                load-api.txt:4: error: Attempted to change parameter name from bread to toast in method test.pkg.Foo.bar [ParameterNameChange]
-            """,
-            signatureSource =
-                """
-                package test.pkg {
-                  interface Foo {
-                    method public void bar(int toast);
-                  }
-                }
-            """,
-            checkCompatibilityApiReleased =
-                """
-                package test.pkg {
-                  interface Foo {
-                    method public void bar(int bread);
-                  }
-                }
-            """
-        )
-    }
-
     @Test
     fun `Change method name (Incompatible)`() {
         check(
diff --git a/metalava/src/test/java/com/android/tools/metalava/cli/compatibility/CompatibilityCheckOptionsTest.kt b/metalava/src/test/java/com/android/tools/metalava/cli/compatibility/CompatibilityCheckOptionsTest.kt
index 9002996..3127b9d 100644
--- a/metalava/src/test/java/com/android/tools/metalava/cli/compatibility/CompatibilityCheckOptionsTest.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/cli/compatibility/CompatibilityCheckOptionsTest.kt
@@ -16,7 +16,11 @@
 
 package com.android.tools.metalava.cli.compatibility
 
+import com.android.tools.metalava.ApiType
 import com.android.tools.metalava.cli.common.BaseOptionGroupTest
+import com.android.tools.metalava.testing.signature
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
 
 val COMPATIBILITY_CHECK_OPTIONS_HELP =
     """
@@ -30,8 +34,18 @@
                                              @SystemApi txt file), which allows us to recognize when an API is moved
                                              from the partial API to the base API and avoid incorrectly flagging this
   --check-compatibility:api:released <file>  Check compatibility of the previously released API.
+
+                                             When multiple files are provided any files that are a delta on another file
+                                             must come after the other file, e.g. if `system` is a delta on `public`
+                                             then `public` must come first, then `system`. Or, in other words, they must
+                                             be provided in order from the narrowest API to the widest API.
   --check-compatibility:removed:released <file>
                                              Check compatibility of the previously released but since removed APIs.
+
+                                             When multiple files are provided any files that are a delta on another file
+                                             must come after the other file, e.g. if `system` is a delta on `public`
+                                             then `public` must come first, then `system`. Or, in other words, they must
+                                             be provided in order from the narrowest API to the widest API.
   --error-message:compatibility:released <message>
                                              If set, this is output when errors are detected in
                                              --check-compatibility:api:released or
@@ -45,4 +59,64 @@
     ) {
 
     override fun createOptions(): CompatibilityCheckOptions = CompatibilityCheckOptions()
+
+    @Test
+    fun `check compatibility api released`() {
+        val file =
+            signature("released.txt", "// Signature format: 2.0\n").createFile(temporaryFolder.root)
+        runTest(ARG_CHECK_COMPATIBILITY_API_RELEASED, file.path) {
+            assertThat(options.compatibilityChecks)
+                .isEqualTo(
+                    listOf(
+                        CompatibilityCheckOptions.CheckRequest(
+                            files = listOf(file),
+                            apiType = ApiType.PUBLIC_API,
+                        ),
+                    )
+                )
+        }
+    }
+
+    @Test
+    fun `check compatibility api released multiple files`() {
+        val file1 =
+            signature("released1.txt", "// Signature format: 2.0\n")
+                .createFile(temporaryFolder.root)
+        val file2 =
+            signature("released2.txt", "// Signature format: 2.0\n")
+                .createFile(temporaryFolder.root)
+        runTest(
+            ARG_CHECK_COMPATIBILITY_API_RELEASED,
+            file1.path,
+            ARG_CHECK_COMPATIBILITY_API_RELEASED,
+            file2.path,
+        ) {
+            assertThat(options.compatibilityChecks)
+                .isEqualTo(
+                    listOf(
+                        CompatibilityCheckOptions.CheckRequest(
+                            files = listOf(file1, file2),
+                            apiType = ApiType.PUBLIC_API,
+                        ),
+                    )
+                )
+        }
+    }
+
+    @Test
+    fun `check compatibility removed api released`() {
+        val file =
+            signature("removed.txt", "// Signature format: 2.0\n").createFile(temporaryFolder.root)
+        runTest(ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED, file.path) {
+            assertThat(options.compatibilityChecks)
+                .isEqualTo(
+                    listOf(
+                        CompatibilityCheckOptions.CheckRequest(
+                            files = listOf(file),
+                            apiType = ApiType.REMOVED,
+                        ),
+                    )
+                )
+        }
+    }
 }
diff --git a/metalava/src/test/java/com/android/tools/metalava/cli/help/IssuesCommandTest.kt b/metalava/src/test/java/com/android/tools/metalava/cli/help/IssuesCommandTest.kt
index 8b05848..2a872d9 100644
--- a/metalava/src/test/java/com/android/tools/metalava/cli/help/IssuesCommandTest.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/cli/help/IssuesCommandTest.kt
@@ -96,6 +96,7 @@
   ExpectedPlatformType                       |  unknown                 |   hidden
   ExtendsDeprecated                          |  unknown                 |   hidden
   ExtendsError                               |  api_lint                |   error
+  FlaggedApiLiteral                          |  api_lint                |   hidden
   ForbiddenSuperClass                        |  api_lint                |   error
   ForbiddenTag                               |  unknown                 |   error
   FractionFloat                              |  api_lint                |   error
diff --git a/metalava/src/test/java/com/android/tools/metalava/cli/signature/MergeSignaturesCommandTest.kt b/metalava/src/test/java/com/android/tools/metalava/cli/signature/MergeSignaturesCommandTest.kt
index 3bfc5e4..e941b86 100644
--- a/metalava/src/test/java/com/android/tools/metalava/cli/signature/MergeSignaturesCommandTest.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/cli/signature/MergeSignaturesCommandTest.kt
@@ -365,7 +365,8 @@
 
         val source2 =
             """
-                // Signature format: 3.0
+                // Signature format: 5.0
+                // - kotlin-style-nulls=no
                 package Test.pkg {
                 }
             """
diff --git a/metalava/src/test/java/com/android/tools/metalava/compatibility/CompatibilityCheckTest.kt b/metalava/src/test/java/com/android/tools/metalava/compatibility/CompatibilityCheckTest.kt
index 7c8ceb0..643e862 100644
--- a/metalava/src/test/java/com/android/tools/metalava/compatibility/CompatibilityCheckTest.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/compatibility/CompatibilityCheckTest.kt
@@ -30,7 +30,6 @@
 import com.android.tools.metalava.nonNullSource
 import com.android.tools.metalava.reporter.Issues
 import com.android.tools.metalava.restrictToSource
-import com.android.tools.metalava.supportParameterName
 import com.android.tools.metalava.suppressLintSource
 import com.android.tools.metalava.systemApiSource
 import com.android.tools.metalava.testApiSource
@@ -252,77 +251,6 @@
     }
 
     @Test
-    fun `Java Parameter Name Change`() {
-        check(
-            expectedIssues =
-                """
-                src/test/pkg/JavaClass.java:6: error: Attempted to remove parameter name from parameter newName in test.pkg.JavaClass.method1 [ParameterNameChange]
-                src/test/pkg/JavaClass.java:7: error: Attempted to change parameter name from secondParameter to newName in method test.pkg.JavaClass.method2 [ParameterNameChange]
-                """,
-            checkCompatibilityApiReleased =
-                """
-                package test.pkg {
-                  public class JavaClass {
-                    ctor public JavaClass();
-                    method public String method1(String parameterName);
-                    method public String method2(String firstParameter, String secondParameter);
-                  }
-                }
-                """,
-            sourceFiles =
-                arrayOf(
-                    java(
-                        """
-                    @Suppress("all")
-                    package test.pkg;
-                    import androidx.annotation.ParameterName;
-
-                    public class JavaClass {
-                        public String method1(String newName) { return null; }
-                        public String method2(@ParameterName("firstParameter") String s, @ParameterName("newName") String prevName) { return null; }
-                    }
-                    """
-                    ),
-                    supportParameterName
-                ),
-            extraArguments = arrayOf(ARG_HIDE_PACKAGE, "androidx.annotation")
-        )
-    }
-
-    @Test
-    fun `Kotlin Parameter Name Change`() {
-        check(
-            expectedIssues =
-                """
-                src/test/pkg/KotlinClass.kt:4: error: Attempted to change parameter name from prevName to newName in method test.pkg.KotlinClass.method1 [ParameterNameChange]
-                """,
-            format = FileFormat.V2,
-            checkCompatibilityApiReleased =
-                """
-                // Signature format: 3.0
-                package test.pkg {
-                  public final class KotlinClass {
-                    ctor public KotlinClass();
-                    method public final String? method1(String prevName);
-                  }
-                }
-                """,
-            sourceFiles =
-                arrayOf(
-                    kotlin(
-                        """
-                    package test.pkg
-
-                    class KotlinClass {
-                        fun method1(newName: String): String? = null
-                    }
-                    """
-                    )
-                )
-        )
-    }
-
-    @Test
     fun `Kotlin Coroutines`() {
         check(
             expectedIssues = "",
@@ -1685,98 +1613,6 @@
     }
 
     @Test
-    fun `Incompatible method change -- throws list -- java`() {
-        check(
-            expectedIssues =
-                """
-                src/test/pkg/MyClass.java:7: error: Method test.pkg.MyClass.method1 added thrown exception java.io.IOException [ChangedThrows]
-                src/test/pkg/MyClass.java:8: error: Method test.pkg.MyClass.method2 no longer throws exception java.io.IOException [ChangedThrows]
-                src/test/pkg/MyClass.java:9: error: Method test.pkg.MyClass.method3 no longer throws exception java.io.IOException [ChangedThrows]
-                src/test/pkg/MyClass.java:9: error: Method test.pkg.MyClass.method3 no longer throws exception java.lang.NumberFormatException [ChangedThrows]
-                src/test/pkg/MyClass.java:9: error: Method test.pkg.MyClass.method3 added thrown exception java.lang.UnsupportedOperationException [ChangedThrows]
-                """,
-            checkCompatibilityApiReleased =
-                """
-                package test.pkg {
-                  public abstract class MyClass {
-                      method public void finalize() throws java.lang.Throwable;
-                      method public void method1();
-                      method public void method2() throws java.io.IOException;
-                      method public void method3() throws java.io.IOException, java.lang.NumberFormatException;
-                  }
-                }
-                """,
-            sourceFiles =
-                arrayOf(
-                    java(
-                        """
-                    package test.pkg;
-
-                    @SuppressWarnings("RedundantThrows")
-                    public abstract class MyClass {
-                        private MyClass() {}
-                        public void finalize() {}
-                        public void method1() throws java.io.IOException {}
-                        public void method2() {}
-                        public void method3() throws java.lang.UnsupportedOperationException {}
-                    }
-                    """
-                    )
-                )
-        )
-    }
-
-    @Test
-    fun `Incompatible method change -- throws list -- kt`() {
-        check(
-            expectedIssues =
-                """
-                src/test/pkg/MyClass.kt:4: error: Constructor test.pkg.MyClass added thrown exception test.pkg.MyException [ChangedThrows]
-                src/test/pkg/MyClass.kt:12: error: Method test.pkg.MyClass.getProperty1 added thrown exception test.pkg.MyException [ChangedThrows]
-                src/test/pkg/MyClass.kt:15: error: Method test.pkg.MyClass.getProperty2 added thrown exception test.pkg.MyException [ChangedThrows]
-                src/test/pkg/MyClass.kt:9: error: Method test.pkg.MyClass.method1 added thrown exception test.pkg.MyException [ChangedThrows]
-            """,
-            checkCompatibilityApiReleased =
-                """
-                package test.pkg {
-                  public final class MyClass {
-                    ctor public MyClass(int);
-                    method public final void method1();
-                    method public final String getProperty1();
-                    method public final String getProperty2();
-                  }
-                }
-            """,
-            sourceFiles =
-                arrayOf(
-                    kotlin(
-                        """
-                        package test.pkg
-
-                        class MyClass
-                        @Throws(MyException::class)
-                        constructor(
-                            val p: Int
-                        ) {
-                            @Throws(MyException::class)
-                            fun method1() {}
-
-                            @get:Throws(MyException::class)
-                            val property1 : String = "42"
-
-                            val property2 : String = "42"
-                                @Throws(MyException::class)
-                                get
-                        }
-
-                        class MyException : Exception()
-                    """
-                    )
-                )
-        )
-    }
-
-    @Test
     fun `Incompatible method change -- return types`() {
         check(
             expectedIssues =
@@ -2440,81 +2276,6 @@
     }
 
     @Test
-    fun `Partial text file where type previously did not exist`() {
-        check(
-            expectedIssues = """
-                """,
-            sourceFiles =
-                arrayOf(
-                    java(
-                            """
-                    package test.pkg;
-                    import android.annotation.SystemApi;
-
-                    /**
-                     * @hide
-                     */
-                    @SystemApi
-                    public class SampleException1 extends java.lang.Exception {
-                    }
-                    """
-                        )
-                        .indented(),
-                    java(
-                            """
-                    package test.pkg;
-                    import android.annotation.SystemApi;
-
-                    /**
-                     * @hide
-                     */
-                    @SystemApi
-                    public class SampleException2 extends java.lang.Throwable {
-                    }
-                    """
-                        )
-                        .indented(),
-                    java(
-                        """
-                    package test.pkg;
-                    import android.annotation.SystemApi;
-
-                    /**
-                     * @hide
-                     */
-                    @SystemApi
-                    public class Utils {
-                        public void method1() throws SampleException1 { }
-                        public void method2() throws SampleException2 { }
-                    }
-                    """
-                    ),
-                    systemApiSource
-                ),
-            extraArguments =
-                arrayOf(
-                    ARG_SHOW_ANNOTATION,
-                    "android.annotation.SystemApi",
-                    ARG_HIDE_PACKAGE,
-                    "android.annotation",
-                ),
-            checkCompatibilityApiReleased =
-                """
-                package test.pkg {
-                  public class Utils {
-                    ctor public Utils();
-                    // We don't define SampleException1 or SampleException in this file,
-                    // in this partial signature, so we don't need to validate that they
-                    // have not been changed
-                    method public void method1() throws test.pkg.SampleException1;
-                    method public void method2() throws test.pkg.SampleException2;
-                  }
-                }
-                """
-        )
-    }
-
-    @Test
     fun `Regression test for bug 120847535`() {
         // Regression test for
         // 120847535: check-api doesn't fail on method that is in current.txt, but marked @hide
@@ -4805,25 +4566,25 @@
             expectedIssues =
                 """
                 error: Method test.pkg.MyCollection.add has changed 'abstract' qualifier [ChangedAbstract]
-                error: Attempted to change parameter name from e to p in method test.pkg.MyCollection.add [ParameterNameChange]
+                error: Attempted to remove parameter name from parameter p in test.pkg.MyCollection.add [ParameterNameChange]
                 error: Method test.pkg.MyCollection.addAll has changed 'abstract' qualifier [ChangedAbstract]
-                error: Attempted to change parameter name from c to p in method test.pkg.MyCollection.addAll [ParameterNameChange]
+                error: Attempted to remove parameter name from parameter p in test.pkg.MyCollection.addAll [ParameterNameChange]
                 error: Method test.pkg.MyCollection.clear has changed 'abstract' qualifier [ChangedAbstract]
                 load-api.txt:5: error: Attempted to change parameter name from o to element in method test.pkg.MyCollection.contains [ParameterNameChange]
                 load-api.txt:5: error: Attempted to change parameter name from o to element in method test.pkg.MyCollection.contains [ParameterNameChange]
                 load-api.txt:6: error: Attempted to change parameter name from c to elements in method test.pkg.MyCollection.containsAll [ParameterNameChange]
                 load-api.txt:6: error: Attempted to change parameter name from c to elements in method test.pkg.MyCollection.containsAll [ParameterNameChange]
                 error: Method test.pkg.MyCollection.remove has changed 'abstract' qualifier [ChangedAbstract]
-                error: Attempted to change parameter name from o to p in method test.pkg.MyCollection.remove [ParameterNameChange]
+                error: Attempted to remove parameter name from parameter p in test.pkg.MyCollection.remove [ParameterNameChange]
                 error: Method test.pkg.MyCollection.removeAll has changed 'abstract' qualifier [ChangedAbstract]
-                error: Attempted to change parameter name from c to p in method test.pkg.MyCollection.removeAll [ParameterNameChange]
+                error: Attempted to remove parameter name from parameter p in test.pkg.MyCollection.removeAll [ParameterNameChange]
                 error: Method test.pkg.MyCollection.retainAll has changed 'abstract' qualifier [ChangedAbstract]
-                error: Attempted to change parameter name from c to p in method test.pkg.MyCollection.retainAll [ParameterNameChange]
+                error: Attempted to remove parameter name from parameter p in test.pkg.MyCollection.retainAll [ParameterNameChange]
                 error: Method test.pkg.MyCollection.size has changed 'abstract' qualifier [ChangedAbstract]
                 error: Method test.pkg.MyCollection.toArray has changed 'abstract' qualifier [ChangedAbstract]
                 error: Method test.pkg.MyCollection.toArray has changed 'abstract' qualifier [ChangedAbstract]
-                error: Attempted to change parameter name from a to p in method test.pkg.MyCollection.toArray [ParameterNameChange]
-            """,
+                error: Attempted to remove parameter name from parameter p in test.pkg.MyCollection.toArray [ParameterNameChange]
+                """,
             checkCompatibilityApiReleased =
                 """
                 // Signature format: 4.0
@@ -4865,37 +4626,6 @@
     }
 
     @Test
-    fun `Flag renaming a parameter from the classpath`() {
-        check(
-            apiClassResolution = ApiClassResolution.API_CLASSPATH,
-            expectedIssues =
-                """
-                error: Attempted to change parameter name from prefix to suffix in method test.pkg.MyString.endsWith [ParameterNameChange]
-                load-api.txt:4: error: Attempted to change parameter name from prefix to suffix in method test.pkg.MyString.startsWith [ParameterNameChange]
-            """
-                    .trimIndent(),
-            checkCompatibilityApiReleased =
-                """
-                // Signature format: 4.0
-                package test.pkg {
-                    public class MyString extends java.lang.String {
-                        method public boolean endsWith(String prefix);
-                    }
-                }
-            """,
-            signatureSource =
-                """
-                // Signature format: 4.0
-                package test.pkg {
-                    public class MyString extends java.lang.String {
-                        method public boolean startsWith(String suffix);
-                    }
-                }
-            """
-        )
-    }
-
-    @Test
     fun `No issues using the same classpath class twice`() {
         check(
             apiClassResolution = ApiClassResolution.API_CLASSPATH,
diff --git a/metalava/src/test/java/com/android/tools/metalava/compatibility/MultipleCompatibilityFilesTest.kt b/metalava/src/test/java/com/android/tools/metalava/compatibility/MultipleCompatibilityFilesTest.kt
new file mode 100644
index 0000000..d38e681
--- /dev/null
+++ b/metalava/src/test/java/com/android/tools/metalava/compatibility/MultipleCompatibilityFilesTest.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.compatibility
+
+import com.android.tools.metalava.DriverTest
+import org.junit.Test
+
+class MultipleCompatibilityFilesTest : DriverTest() {
+
+    private val previouslyReleasedPublicApi =
+        """
+            // Signature format: 2.0
+            package test.pkg {
+              public class Bar extends IllegalStateException {
+              }
+              public class Foo {
+                method public void foo() throws Bar;
+              }
+            }
+        """
+
+    private val previouslyReleasedSystemApiDelta =
+        """
+            // Signature format: 2.0
+            package test.pkg {
+              public class Baz extends test.pkg.Bar {
+              }
+              public class Foo {
+                method public void foo() throws Baz;
+              }
+            }
+        """
+
+    /**
+     * The current and complete system api which will be tested for compatibility against some
+     * combination of the above previously released APIs.
+     */
+    private val currentCompleteSystemApi =
+        """
+            package test.pkg {
+              public class Bar extends IllegalStateException {
+              }
+              public class Baz extends test.pkg.Bar {
+              }
+              public class Foo {
+                method public void foo() throws Baz;
+              }
+            }
+        """
+
+    @Test
+    fun `Test public only`() {
+        check(
+            checkCompatibilityApiReleasedList = listOf(previouslyReleasedPublicApi),
+            signatureSource = currentCompleteSystemApi,
+            // This fails because the previous system API is not provided.
+            expectedIssues =
+                """
+                    load-api.txt:8: error: Method test.pkg.Foo.foo no longer throws exception Bar [ChangedThrows]
+                    load-api.txt:8: error: Method test.pkg.Foo.foo added thrown exception Baz [ChangedThrows]
+                """,
+        )
+    }
+
+    @Test
+    fun `Test system only`() {
+        check(
+            checkCompatibilityApiReleasedList = listOf(previouslyReleasedSystemApiDelta),
+            signatureSource = currentCompleteSystemApi,
+        )
+    }
+
+    @Test
+    fun `Test multiple compatibility files (public first)`() {
+        check(
+            checkCompatibilityApiReleasedList =
+                listOf(previouslyReleasedPublicApi, previouslyReleasedSystemApiDelta),
+            signatureSource = currentCompleteSystemApi,
+        )
+    }
+
+    @Test
+    fun `Test multiple compatibility files (system first)`() {
+        check(
+            checkCompatibilityApiReleasedList =
+                listOf(previouslyReleasedSystemApiDelta, previouslyReleasedPublicApi),
+            signatureSource = currentCompleteSystemApi,
+            // This fails because the previous system API is provided first and the
+            // CompatibilityCheck assumes that the second one should override the first, so it is
+            // using the public definition of the `foo()` method and the public `throws` list which
+            // is why it is getting the same errors as when public only is provided.
+            expectedIssues =
+                """
+                    load-api.txt:8: error: Method test.pkg.Foo.foo no longer throws exception Bar [ChangedThrows]
+                    load-api.txt:8: error: Method test.pkg.Foo.foo added thrown exception Baz [ChangedThrows]
+                """,
+        )
+    }
+}
diff --git a/metalava/src/test/java/com/android/tools/metalava/compatibility/ParameterNameChangeTest.kt b/metalava/src/test/java/com/android/tools/metalava/compatibility/ParameterNameChangeTest.kt
new file mode 100644
index 0000000..242ef6d
--- /dev/null
+++ b/metalava/src/test/java/com/android/tools/metalava/compatibility/ParameterNameChangeTest.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.compatibility
+
+import com.android.tools.metalava.ARG_HIDE_PACKAGE
+import com.android.tools.metalava.DriverTest
+import com.android.tools.metalava.model.text.ApiClassResolution
+import com.android.tools.metalava.model.text.FileFormat
+import com.android.tools.metalava.supportParameterName
+import com.android.tools.metalava.testing.java
+import com.android.tools.metalava.testing.kotlin
+import org.junit.Test
+
+class ParameterNameChangeTest : DriverTest() {
+
+    @Test
+    fun `Change formal parameter name class method (Incompatible)`() {
+        check(
+            expectedIssues =
+                """
+                    load-api.txt:4: error: Attempted to change parameter name from bread to toast in method test.pkg.Foo.bar [ParameterNameChange]
+                """,
+            signatureSource =
+                """
+                    package test.pkg {
+                      class Foo {
+                        method public void bar(Int toast);
+                      }
+                    }
+                """,
+            checkCompatibilityApiReleased =
+                """
+                    package test.pkg {
+                      class Foo {
+                        method public void bar(Int bread);
+                      }
+                    }
+                """,
+        )
+    }
+
+    @Test
+    fun `Change formal parameter name interface method (Incompatible)`() {
+        check(
+            expectedIssues =
+                """
+                    load-api.txt:4: error: Attempted to change parameter name from bread to toast in method test.pkg.Foo.bar [ParameterNameChange]
+                """,
+            signatureSource =
+                """
+                    package test.pkg {
+                      interface Foo {
+                        method public void bar(int toast);
+                      }
+                    }
+                """,
+            checkCompatibilityApiReleased =
+                """
+                    package test.pkg {
+                      interface Foo {
+                        method public void bar(int bread);
+                      }
+                    }
+                """,
+        )
+    }
+
+    @Test
+    fun `Flag renaming a parameter from the classpath`() {
+        check(
+            apiClassResolution = ApiClassResolution.API_CLASSPATH,
+            expectedIssues =
+                """
+                    error: Attempted to change parameter name from prefix to suffix in method test.pkg.MyString.endsWith [ParameterNameChange]
+                    load-api.txt:4: error: Attempted to change parameter name from prefix to suffix in method test.pkg.MyString.startsWith [ParameterNameChange]
+                """
+                    .trimIndent(),
+            checkCompatibilityApiReleased =
+                """
+                    // Signature format: 4.0
+                    package test.pkg {
+                        public class MyString extends java.lang.String {
+                            method public boolean endsWith(String prefix);
+                        }
+                    }
+                """,
+            signatureSource =
+                """
+                    // Signature format: 4.0
+                    package test.pkg {
+                        public class MyString extends java.lang.String {
+                            method public boolean startsWith(String suffix);
+                        }
+                    }
+                """,
+        )
+    }
+
+    @Test
+    fun `Java Parameter Name Change`() {
+        check(
+            expectedIssues =
+                """
+                    src/test/pkg/JavaClass.java:6: error: Attempted to remove parameter name from parameter newName in test.pkg.JavaClass.method1 [ParameterNameChange]
+                    src/test/pkg/JavaClass.java:7: error: Attempted to change parameter name from secondParameter to newName in method test.pkg.JavaClass.method2 [ParameterNameChange]
+                """,
+            checkCompatibilityApiReleased =
+                """
+                    package test.pkg {
+                      public class JavaClass {
+                        ctor public JavaClass();
+                        method public String method1(String parameterName);
+                        method public String method2(String firstParameter, String secondParameter);
+                      }
+                    }
+                """,
+            sourceFiles =
+                arrayOf(
+                    java(
+                        """
+                            @Suppress("all")
+                            package test.pkg;
+                            import androidx.annotation.ParameterName;
+
+                            public class JavaClass {
+                                public String method1(String newName) { return null; }
+                                public String method2(@ParameterName("firstParameter") String s, @ParameterName("newName") String prevName) { return null; }
+                            }
+                        """
+                    ),
+                    supportParameterName
+                ),
+            extraArguments = arrayOf(ARG_HIDE_PACKAGE, "androidx.annotation"),
+        )
+    }
+
+    @Test
+    fun `Kotlin Parameter Name Change`() {
+        check(
+            expectedIssues =
+                """
+                    src/test/pkg/KotlinClass.kt:4: error: Attempted to change parameter name from prevName to newName in method test.pkg.KotlinClass.method1 [ParameterNameChange]
+                """,
+            format = FileFormat.V2,
+            checkCompatibilityApiReleased =
+                """
+                    // Signature format: 3.0
+                    package test.pkg {
+                      public final class KotlinClass {
+                        ctor public KotlinClass();
+                        method public final String? method1(String prevName);
+                      }
+                    }
+                """,
+            sourceFiles =
+                arrayOf(
+                    kotlin(
+                        """
+                            package test.pkg
+
+                            class KotlinClass {
+                                fun method1(newName: String): String? = null
+                            }
+                        """
+                    )
+                ),
+        )
+    }
+}
diff --git a/metalava/src/test/java/com/android/tools/metalava/compatibility/ThrowsCompatibilityTest.kt b/metalava/src/test/java/com/android/tools/metalava/compatibility/ThrowsCompatibilityTest.kt
new file mode 100644
index 0000000..3d9b291
--- /dev/null
+++ b/metalava/src/test/java/com/android/tools/metalava/compatibility/ThrowsCompatibilityTest.kt
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.compatibility
+
+import com.android.tools.metalava.ARG_HIDE_PACKAGE
+import com.android.tools.metalava.ARG_SHOW_ANNOTATION
+import com.android.tools.metalava.DriverTest
+import com.android.tools.metalava.systemApiSource
+import com.android.tools.metalava.testing.java
+import com.android.tools.metalava.testing.kotlin
+import org.junit.Test
+
+class ThrowsCompatibilityTest : DriverTest() {
+    @Test
+    fun `Incompatible method change -- throws list -- java`() {
+        check(
+            expectedIssues =
+                """
+                    src/test/pkg/MyClass.java:7: error: Method test.pkg.MyClass.method1 added thrown exception java.io.IOException [ChangedThrows]
+                    src/test/pkg/MyClass.java:8: error: Method test.pkg.MyClass.method2 no longer throws exception java.io.IOException [ChangedThrows]
+                    src/test/pkg/MyClass.java:9: error: Method test.pkg.MyClass.method3 no longer throws exception java.io.IOException [ChangedThrows]
+                    src/test/pkg/MyClass.java:9: error: Method test.pkg.MyClass.method3 no longer throws exception java.lang.NumberFormatException [ChangedThrows]
+                    src/test/pkg/MyClass.java:9: error: Method test.pkg.MyClass.method3 added thrown exception java.lang.UnsupportedOperationException [ChangedThrows]
+                """,
+            checkCompatibilityApiReleased =
+                """
+                    package test.pkg {
+                      public abstract class MyClass {
+                          method public void finalize() throws java.lang.Throwable;
+                          method public void method1();
+                          method public void method2() throws java.io.IOException;
+                          method public void method3() throws java.io.IOException, java.lang.NumberFormatException;
+                      }
+                    }
+                """,
+            sourceFiles =
+                arrayOf(
+                    java(
+                        """
+                            package test.pkg;
+
+                            @SuppressWarnings("RedundantThrows")
+                            public abstract class MyClass {
+                                private MyClass() {}
+                                public void finalize() {}
+                                public void method1() throws java.io.IOException {}
+                                public void method2() {}
+                                public void method3() throws java.lang.UnsupportedOperationException {}
+                            }
+                        """
+                    )
+                ),
+        )
+    }
+
+    @Test
+    fun `Incompatible method change -- throws list -- kt`() {
+        check(
+            expectedIssues =
+                """
+                    src/test/pkg/MyClass.kt:4: error: Constructor test.pkg.MyClass added thrown exception test.pkg.MyException [ChangedThrows]
+                    src/test/pkg/MyClass.kt:12: error: Method test.pkg.MyClass.getProperty1 added thrown exception test.pkg.MyException [ChangedThrows]
+                    src/test/pkg/MyClass.kt:15: error: Method test.pkg.MyClass.getProperty2 added thrown exception test.pkg.MyException [ChangedThrows]
+                    src/test/pkg/MyClass.kt:9: error: Method test.pkg.MyClass.method1 added thrown exception test.pkg.MyException [ChangedThrows]
+                """,
+            checkCompatibilityApiReleased =
+                """
+                    package test.pkg {
+                      public final class MyClass {
+                        ctor public MyClass(int);
+                        method public final void method1();
+                        method public final String getProperty1();
+                        method public final String getProperty2();
+                      }
+                    }
+                """,
+            sourceFiles =
+                arrayOf(
+                    kotlin(
+                        """
+                            package test.pkg
+
+                            class MyClass
+                            @Throws(MyException::class)
+                            constructor(
+                                val p: Int
+                            ) {
+                                @Throws(MyException::class)
+                                fun method1() {}
+
+                                @get:Throws(MyException::class)
+                                val property1 : String = "42"
+
+                                val property2 : String = "42"
+                                    @Throws(MyException::class)
+                                    get
+                            }
+
+                            class MyException : Exception()
+                        """
+                    )
+                ),
+        )
+    }
+
+    @Test
+    fun `Incompatible method change -- throws list -- type parameter`() {
+        check(
+            expectedIssues =
+                """
+                    src/test/pkg/MyClass.java:7: error: Method test.pkg.MyClass.method1 added thrown exception T (extends java.lang.Throwable)} [ChangedThrows]
+                    src/test/pkg/MyClass.java:8: error: Method test.pkg.MyClass.method2 no longer throws exception T (extends java.lang.Throwable)} [ChangedThrows]
+                    src/test/pkg/MyClass.java:9: error: Method test.pkg.MyClass.method3 added thrown exception X (extends java.io.FileNotFoundException)} [ChangedThrows]
+                    src/test/pkg/MyClass.java:10: error: Method test.pkg.MyClass.method4 no longer throws exception X (extends java.io.FileNotFoundException)} [ChangedThrows]
+                    src/test/pkg/MyClass.java:10: error: Method test.pkg.MyClass.method4 added thrown exception X (extends java.io.IOException)} [ChangedThrows]
+                """,
+            checkCompatibilityApiReleased =
+                """
+                    package test.pkg {
+                      public abstract class MyClass<T extends Throwable> {
+                          method public void finalize() throws T;
+                          method public void method1();
+                          method public void method2() throws T;
+                          method public <X extends java.io.IOException> void method3() throws X;
+                          method public <X extends java.io.FileNotFoundException> void method4() throws X;
+                          method public <X extends java.io.IOException> void method5() throws X;
+                      }
+                    }
+                """,
+            sourceFiles =
+                arrayOf(
+                    java(
+                        """
+                            package test.pkg;
+
+                            @SuppressWarnings("RedundantThrows")
+                            public abstract class MyClass<T extends Throwable> {
+                                private MyClass() {}
+                                public void finalize() {}
+                                public void method1() throws T {}
+                                public void method2() {}
+                                public <X extends java.io.FileNotFoundException> void method3() throws X;
+                                public <X extends java.io.IOException> void method4() throws X;
+                                public <Y extends java.io.IOException> void method5() throws Y;
+                            }
+                        """
+                    )
+                ),
+        )
+    }
+
+    @Test
+    fun `Partial text file where type previously did not exist`() {
+        check(
+            sourceFiles =
+                arrayOf(
+                    java(
+                        """
+                            package test.pkg;
+                            import android.annotation.SystemApi;
+
+                            /**
+                             * @hide
+                             */
+                            @SystemApi
+                            public class SampleException1 extends java.lang.Exception {
+                            }
+                        """
+                    ),
+                    java(
+                        """
+                            package test.pkg;
+                            import android.annotation.SystemApi;
+
+                            /**
+                             * @hide
+                             */
+                            @SystemApi
+                            public class SampleException2 extends java.lang.Throwable {
+                            }
+                        """
+                    ),
+                    java(
+                        """
+                            package test.pkg;
+                            import android.annotation.SystemApi;
+
+                            /**
+                             * @hide
+                             */
+                            @SystemApi
+                            public class Utils {
+                                public void method1() throws SampleException1 { }
+                                public void method2() throws SampleException2 { }
+                            }
+                        """
+                    ),
+                    systemApiSource
+                ),
+            extraArguments =
+                arrayOf(
+                    ARG_SHOW_ANNOTATION,
+                    "android.annotation.SystemApi",
+                    ARG_HIDE_PACKAGE,
+                    "android.annotation",
+                ),
+            checkCompatibilityApiReleased =
+                """
+                    package test.pkg {
+                      public class Utils {
+                        ctor public Utils();
+                        // We don't define SampleException1 or SampleException in this file,
+                        // in this partial signature, so we don't need to validate that they
+                        // have not been changed
+                        method public void method1() throws test.pkg.SampleException1;
+                        method public void method2() throws test.pkg.SampleException2;
+                      }
+                    }
+                """,
+        )
+    }
+}
diff --git a/metalava/src/test/java/com/android/tools/metalava/lint/ApiLintBaselineTest.kt b/metalava/src/test/java/com/android/tools/metalava/lint/ApiLintBaselineTest.kt
index 0093a02..ceba470 100644
--- a/metalava/src/test/java/com/android/tools/metalava/lint/ApiLintBaselineTest.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/lint/ApiLintBaselineTest.kt
@@ -241,12 +241,12 @@
                     public class Base {
                         public static abstract class BaseBuilder<B extends BaseBuilder<B>> {
                             protected int field;
-                            
+
                             protected BaseBuilder() {}
-                            
+
                             @NonNull
                             protected abstract B getThis();
-                            
+
                             @NonNull
                             public B setField(int i) {
                                 this.field = i;
@@ -265,7 +265,7 @@
                         private Foo(int i) {
                             this.field = i;
                         }
-                        
+
                         public static final class Builder extends BaseBuilder<Builder> {
                             public Builder() {}
 
diff --git a/metalava/src/test/java/com/android/tools/metalava/lint/ApiLintTest.kt b/metalava/src/test/java/com/android/tools/metalava/lint/ApiLintTest.kt
index 720f245..0f64671 100644
--- a/metalava/src/test/java/com/android/tools/metalava/lint/ApiLintTest.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/lint/ApiLintTest.kt
@@ -1443,47 +1443,6 @@
     }
 
     @Test
-    fun `Check exception related issues`() {
-        check(
-            extraArguments =
-                arrayOf(
-                    ARG_API_LINT,
-                    // Conflicting advice:
-                    ARG_HIDE,
-                    "BannedThrow"
-                ),
-            expectedIssues =
-                """
-                src/android/pkg/MyClass.java:6: error: Methods must not throw generic exceptions (`java.lang.Exception`) [GenericException]
-                src/android/pkg/MyClass.java:7: error: Methods must not throw generic exceptions (`java.lang.Throwable`) [GenericException]
-                src/android/pkg/MyClass.java:8: error: Methods must not throw generic exceptions (`java.lang.Error`) [GenericException]
-                src/android/pkg/MyClass.java:11: error: Methods calling system APIs should rethrow `RemoteException` as `RuntimeException` (but do not list it in the throws clause) [RethrowRemoteException]
-                """,
-            expectedFail = DefaultLintErrorMessage,
-            sourceFiles =
-                arrayOf(
-                    java(
-                        """
-                    package android.pkg;
-                    import android.os.RemoteException;
-
-                    @SuppressWarnings("RedundantThrows")
-                    public class MyClass {
-                        public void method1() throws Exception { }
-                        public void method2() throws Throwable { }
-                        public void method3() throws Error { }
-                        public void method4() throws IllegalArgumentException { }
-                        public void method4() throws NullPointerException { }
-                        public void method5() throws RemoteException { }
-                        public void ok(int p) throws NullPointerException { }
-                    }
-                    """
-                    )
-                )
-        )
-    }
-
-    @Test
     fun `Check no mentions of Google in APIs`() {
         check(
             apiLint = "", // enabled
@@ -1586,58 +1545,6 @@
     }
 
     @Test
-    fun `Check boxed types`() {
-        check(
-            apiLint = "", // enabled
-            expectedIssues =
-                """
-                src/test/pkg/KotlinClass.kt:4: error: Must avoid boxed primitives (`java.lang.Double`) [AutoBoxing]
-                src/test/pkg/KotlinClass.kt:6: error: Must avoid boxed primitives (`java.lang.Boolean`) [AutoBoxing]
-                src/test/pkg/MyClass.java:9: error: Must avoid boxed primitives (`java.lang.Long`) [AutoBoxing]
-                src/test/pkg/MyClass.java:12: error: Must avoid boxed primitives (`java.lang.Short`) [AutoBoxing]
-                src/test/pkg/MyClass.java:12: error: Must avoid boxed primitives (`java.lang.Double`) [AutoBoxing]
-                src/test/pkg/MyClass.java:14: error: Must avoid boxed primitives (`java.lang.Boolean`) [AutoBoxing]
-                src/test/pkg/MyClass.java:7: error: Must avoid boxed primitives (`java.lang.Integer`) [AutoBoxing]
-                """,
-            expectedFail = DefaultLintErrorMessage,
-            sourceFiles =
-                arrayOf(
-                    java(
-                        """
-                    package test.pkg;
-
-                    import androidx.annotation.Nullable;
-
-                    public class MyClass {
-                        @Nullable
-                        public final Integer integer1;
-                        public final int integer2;
-                        public MyClass(@Nullable Long l) {
-                        }
-                        @Nullable
-                        public Short getDouble(@Nullable Double l) { return null; }
-                        @Nullable
-                        public Boolean getBoolean() { return null; }
-                    }
-                    """
-                    ),
-                    kotlin(
-                        """
-                    package test.pkg
-                    class KotlinClass {
-                        fun getIntegerOk(): Double { TODO() }
-                        fun getIntegerBad(): Double? { TODO() }
-                        fun getBooleanOk(): Boolean { TODO() }
-                        fun getBooleanBad(): Boolean? { TODO() }
-                    }
-                """
-                    ),
-                    androidxNullableSource
-                )
-        )
-    }
-
-    @Test
     fun `Check static utilities`() {
         check(
             apiLint = "", // enabled
@@ -3562,110 +3469,6 @@
     }
 
     @Test
-    fun `Unchecked exceptions not allowed`() {
-        check(
-            expectedIssues =
-                """
-                src/test/pkg/Foo.java:22: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:23: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:24: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:25: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:26: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:27: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:28: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:29: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:30: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:31: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:32: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:33: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:34: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:35: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:36: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:37: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:38: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:39: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:40: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:41: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:42: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:43: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:44: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:45: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:46: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:47: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:48: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:49: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:50: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:51: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:52: error: Methods must not throw unchecked exceptions [BannedThrow]
-                src/test/pkg/Foo.java:53: error: Methods must not throw unchecked exceptions [BannedThrow]
-            """,
-            apiLint = "",
-            expectedFail = DefaultLintErrorMessage,
-            sourceFiles =
-                arrayOf(
-                    java(
-                        """
-                        package test.pkg;
-                        import java.lang.reflect.UndeclaredThrowableException;
-                        import java.lang.reflect.MalformedParametersException;
-                        import java.lang.reflect.MalformedParameterizedTypeException;
-                        import java.lang.invoke.WrongMethodTypeException;
-                        import java.lang.annotation.AnnotationTypeMismatchException;
-                        import java.lang.annotation.IncompleteAnnotationException;
-                        import java.util.MissingResourceException;
-                        import java.util.EmptyStackException;
-                        import java.util.concurrent.CompletionException;
-                        import java.util.concurrent.RejectedExecutionException;
-                        import java.util.IllformedLocaleException;
-                        import java.util.ConcurrentModificationException;
-                        import java.util.NoSuchElementException;
-                        import java.io.UncheckedIOException;
-                        import java.time.DateTimeException;
-                        import java.security.ProviderException;
-                        import java.nio.BufferUnderflowException;
-                        import java.nio.BufferOverflowException;
-                        public class Foo {
-                            // 32 errors
-                            public void a() throws NullPointerException;
-                            public void b() throws ClassCastException;
-                            public void c() throws IndexOutOfBoundsException;
-                            public void d() throws UndeclaredThrowableException;
-                            public void e() throws MalformedParametersException;
-                            public void f() throws MalformedParameterizedTypeException;
-                            public void g() throws WrongMethodTypeException;
-                            public void h() throws EnumConstantNotPresentException;
-                            public void i() throws IllegalMonitorStateException;
-                            public void j() throws SecurityException;
-                            public void k() throws UnsupportedOperationException;
-                            public void l() throws AnnotationTypeMismatchException;
-                            public void m() throws IncompleteAnnotationException;
-                            public void n() throws TypeNotPresentException;
-                            public void o() throws IllegalStateException;
-                            public void p() throws ArithmeticException;
-                            public void q() throws IllegalArgumentException;
-                            public void r() throws ArrayStoreException;
-                            public void s() throws NegativeArraySizeException;
-                            public void t() throws MissingResourceException;
-                            public void u() throws EmptyStackException;
-                            public void v() throws CompletionException;
-                            public void w() throws RejectedExecutionException;
-                            public void x() throws IllformedLocaleException;
-                            public void y() throws ConcurrentModificationException;
-                            public void z() throws NoSuchElementException;
-                            public void aa() throws UncheckedIOException;
-                            public void ab() throws DateTimeException;
-                            public void ac() throws ProviderException;
-                            public void ad() throws BufferUnderflowException;
-                            public void ae() throws BufferOverflowException;
-                            public void af() throws AssertionError;
-                        }
-                    """
-                    ),
-                )
-        )
-    }
-
-    @Test
     fun `Nullability overrides in unbounded generics should be allowed`() {
         check(
             apiLint = "",
diff --git a/metalava/src/test/java/com/android/tools/metalava/lint/AutoBoxingTest.kt b/metalava/src/test/java/com/android/tools/metalava/lint/AutoBoxingTest.kt
new file mode 100644
index 0000000..13bbae9
--- /dev/null
+++ b/metalava/src/test/java/com/android/tools/metalava/lint/AutoBoxingTest.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.lint
+
+import com.android.tools.metalava.DriverTest
+import com.android.tools.metalava.androidxNullableSource
+import com.android.tools.metalava.testing.java
+import com.android.tools.metalava.testing.kotlin
+import org.junit.Test
+
+class AutoBoxingTest : DriverTest() {
+
+    @Test
+    fun `Check boxed types`() {
+        check(
+            apiLint = "", // enabled
+            expectedIssues =
+                """
+                src/test/pkg/KotlinClass.kt:4: error: Must avoid boxed primitives (`java.lang.Double`) [AutoBoxing]
+                src/test/pkg/KotlinClass.kt:6: error: Must avoid boxed primitives (`java.lang.Boolean`) [AutoBoxing]
+                src/test/pkg/MyClass.java:9: error: Must avoid boxed primitives (`java.lang.Long`) [AutoBoxing]
+                src/test/pkg/MyClass.java:12: error: Must avoid boxed primitives (`java.lang.Short`) [AutoBoxing]
+                src/test/pkg/MyClass.java:12: error: Must avoid boxed primitives (`java.lang.Double`) [AutoBoxing]
+                src/test/pkg/MyClass.java:14: error: Must avoid boxed primitives (`java.lang.Boolean`) [AutoBoxing]
+                src/test/pkg/MyClass.java:7: error: Must avoid boxed primitives (`java.lang.Integer`) [AutoBoxing]
+                """,
+            expectedFail = DefaultLintErrorMessage,
+            sourceFiles =
+                arrayOf(
+                    java(
+                        """
+                    package test.pkg;
+
+                    import androidx.annotation.Nullable;
+
+                    public class MyClass {
+                        @Nullable
+                        public final Integer integer1;
+                        public final int integer2;
+                        public MyClass(@Nullable Long l) {
+                        }
+                        @Nullable
+                        public Short getDouble(@Nullable Double l) { return null; }
+                        @Nullable
+                        public Boolean getBoolean() { return null; }
+                    }
+                    """
+                    ),
+                    kotlin(
+                        """
+                    package test.pkg
+                    class KotlinClass {
+                        fun getIntegerOk(): Double { TODO() }
+                        fun getIntegerBad(): Double? { TODO() }
+                        fun getBooleanOk(): Boolean { TODO() }
+                        fun getBooleanBad(): Boolean? { TODO() }
+                    }
+                """
+                    ),
+                    androidxNullableSource
+                )
+        )
+    }
+
+    @Test
+    fun `Check boxing of generic`() {
+        check(
+            apiLint = "", // enabled
+            sourceFiles =
+                arrayOf(
+                    java(
+                        """
+                    package test.pkg;
+
+                    public class MyClass<T extends Number> {
+                        public final T field;
+                    }
+                    """
+                    ),
+                    kotlin(
+                        """
+                    package test.pkg
+                    interface KotlinClass<T: Number> {
+                        val property: T
+                    }
+                """
+                    ),
+                )
+        )
+    }
+}
diff --git a/metalava/src/test/java/com/android/tools/metalava/lint/FlaggedApiLintTest.kt b/metalava/src/test/java/com/android/tools/metalava/lint/FlaggedApiLintTest.kt
index a8f96b7..94cb667 100644
--- a/metalava/src/test/java/com/android/tools/metalava/lint/FlaggedApiLintTest.kt
+++ b/metalava/src/test/java/com/android/tools/metalava/lint/FlaggedApiLintTest.kt
@@ -414,4 +414,83 @@
             checkCompilation = true
         )
     }
+
+    @Test
+    fun `Require @FlaggedApi to reference generated fields`() {
+        check(
+            expectedIssues =
+                """
+                src/android/foobar/Bad.java:6: warning: @FlaggedApi contains a string literal, but should reference the field generated by aconfig (android.foobar.Flags.FLAG_MY_FEATURE). [FlaggedApiLiteral]
+                src/android/foobar/Bad.java:10: warning: @FlaggedApi contains a string literal, but should reference the field generated by aconfig (android.foobar.Flags.FLAG_MY_FEATURE). [FlaggedApiLiteral]
+                src/android/foobar/Bad.java:17: warning: @FlaggedApi contains a string literal, but should reference the field generated by aconfig (furthermore, the current flag literal seems to be malformed). [FlaggedApiLiteral]
+                src/android/foobar/Bad.java:19: warning: @FlaggedApi contains a string literal, but should reference the field generated by aconfig (android.foobar.Flags.FLAG_NONEXISTENT_FLAG, however this flag doesn't seem to exist). [FlaggedApiLiteral]
+                src/android/foobar/Bad.java:21: warning: @FlaggedApi contains a string literal, but should reference the field generated by aconfig (android.baz.Flags.FLAG_NON_EXISTENT_PACKAGE, however this flag doesn't seem to exist). [FlaggedApiLiteral]
+                src/android/foobar/Bad.java:8: warning: @FlaggedApi contains a string literal, but should reference the field generated by aconfig (android.foobar.Flags.FLAG_MY_FEATURE). [FlaggedApiLiteral]
+                src/android/foobar/Bad.java:14: warning: @FlaggedApi contains a string literal, but should reference the field generated by aconfig (android.foobar.Flags.FLAG_MY_FEATURE). [FlaggedApiLiteral]
+                src/android/foobar/Bad.java:12: warning: @FlaggedApi contains a string literal, but should reference the field generated by aconfig (android.foobar.Flags.FLAG_MY_FEATURE). [FlaggedApiLiteral]
+                """
+                    .trimIndent(),
+            apiLint = "",
+            sourceFiles =
+                arrayOf(
+                    java(
+                        """
+                        package android.foobar;
+
+                        import android.annotation.FlaggedApi;
+
+                        @FlaggedApi("android.foobar.my_feature")
+                        public class Bad {
+                            @FlaggedApi("android.foobar.my_feature")
+                            public static final String BAD = "bar";
+                            @FlaggedApi("android.foobar.my_feature")
+                            public void bad() {}
+                            @FlaggedApi("android.foobar.my_feature")
+                            public interface BadInterface {}
+                            @FlaggedApi("android.foobar.my_feature")
+                            public @interface BadAnnotation {}
+
+                            @FlaggedApi("malformed/flag")
+                            public void malformed() {}
+                            @FlaggedApi("android.foobar.nonexistent_flag")
+                            public void nonexistentFlag() {}
+                            @FlaggedApi("android.baz.non_existent_package")
+                            public void nonexistentPackage() {}
+                        }
+                    """
+                    ),
+                    java(
+                        """
+                        package android.foobar;
+
+                        import android.annotation.FlaggedApi;
+
+                        @FlaggedApi(android.foobar.Flags.FLAG_MY_FEATURE)
+                        public class Ok {
+                            @FlaggedApi(android.foobar.Flags.FLAG_MY_FEATURE)
+                            public static final String OK = "bar";
+                            @FlaggedApi(android.foobar.Flags.FLAG_MY_FEATURE)
+                            public void ok() {}
+                            @FlaggedApi(android.foobar.Flags.FLAG_MY_FEATURE)
+                            public interface OkInterface {}
+                            @FlaggedApi(android.foobar.Flags.FLAG_MY_FEATURE)
+                            public @interface OkAnnotation {}
+                        }
+                    """
+                    ),
+                    java(
+                        """
+                        package android.foobar;
+
+                        /** @hide */
+                        public class Flags {
+                            public static final String FLAG_MY_FEATURE = "android.foobar.my_feature";
+                        }
+                    """
+                    ),
+                    flaggedApiSource
+                ),
+            extraArguments = arrayOf(ARG_WARNING, "FlaggedApiLiteral")
+        )
+    }
 }
diff --git a/metalava/src/test/java/com/android/tools/metalava/lint/ThrowsLintTest.kt b/metalava/src/test/java/com/android/tools/metalava/lint/ThrowsLintTest.kt
new file mode 100644
index 0000000..1322da7
--- /dev/null
+++ b/metalava/src/test/java/com/android/tools/metalava/lint/ThrowsLintTest.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.lint
+
+import com.android.tools.metalava.ARG_API_LINT
+import com.android.tools.metalava.DriverTest
+import com.android.tools.metalava.cli.common.ARG_HIDE
+import com.android.tools.metalava.testing.java
+import org.junit.Test
+
+class ThrowsLintTest : DriverTest() {
+
+    @Test
+    fun `Check exception related issues`() {
+        check(
+            extraArguments =
+                arrayOf(
+                    ARG_API_LINT,
+                    // Conflicting advice:
+                    ARG_HIDE,
+                    "BannedThrow"
+                ),
+            expectedIssues =
+                """
+                src/android/pkg/MyClass.java:6: error: Methods must not throw generic exceptions (`java.lang.Exception`) [GenericException]
+                src/android/pkg/MyClass.java:7: error: Methods must not throw generic exceptions (`java.lang.Throwable`) [GenericException]
+                src/android/pkg/MyClass.java:8: error: Methods must not throw generic exceptions (`java.lang.Error`) [GenericException]
+                src/android/pkg/MyClass.java:11: error: Methods calling system APIs should rethrow `RemoteException` as `RuntimeException` (but do not list it in the throws clause) [RethrowRemoteException]
+                """,
+            expectedFail = DefaultLintErrorMessage,
+            sourceFiles =
+                arrayOf(
+                    java(
+                        """
+                    package android.pkg;
+                    import android.os.RemoteException;
+
+                    @SuppressWarnings("RedundantThrows")
+                    public class MyClass {
+                        public void method1() throws Exception { }
+                        public void method2() throws Throwable { }
+                        public void method3() throws Error { }
+                        public void method4() throws IllegalArgumentException { }
+                        public void method4() throws NullPointerException { }
+                        public void method5() throws RemoteException { }
+                        public void ok(int p) throws NullPointerException { }
+                    }
+                    """
+                    )
+                )
+        )
+    }
+
+    @Test
+    fun `Unchecked exceptions not allowed`() {
+        check(
+            expectedIssues =
+                """
+                src/test/pkg/Foo.java:22: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:23: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:24: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:25: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:26: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:27: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:28: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:29: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:30: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:31: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:32: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:33: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:34: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:35: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:36: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:37: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:38: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:39: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:40: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:41: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:42: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:43: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:44: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:45: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:46: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:47: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:48: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:49: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:50: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:51: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:52: error: Methods must not throw unchecked exceptions [BannedThrow]
+                src/test/pkg/Foo.java:53: error: Methods must not throw unchecked exceptions [BannedThrow]
+            """,
+            apiLint = "",
+            expectedFail = DefaultLintErrorMessage,
+            sourceFiles =
+                arrayOf(
+                    java(
+                        """
+                        package test.pkg;
+                        import java.lang.reflect.UndeclaredThrowableException;
+                        import java.lang.reflect.MalformedParametersException;
+                        import java.lang.reflect.MalformedParameterizedTypeException;
+                        import java.lang.invoke.WrongMethodTypeException;
+                        import java.lang.annotation.AnnotationTypeMismatchException;
+                        import java.lang.annotation.IncompleteAnnotationException;
+                        import java.util.MissingResourceException;
+                        import java.util.EmptyStackException;
+                        import java.util.concurrent.CompletionException;
+                        import java.util.concurrent.RejectedExecutionException;
+                        import java.util.IllformedLocaleException;
+                        import java.util.ConcurrentModificationException;
+                        import java.util.NoSuchElementException;
+                        import java.io.UncheckedIOException;
+                        import java.time.DateTimeException;
+                        import java.security.ProviderException;
+                        import java.nio.BufferUnderflowException;
+                        import java.nio.BufferOverflowException;
+                        public class Foo {
+                            // 32 errors
+                            public void a() throws NullPointerException;
+                            public void b() throws ClassCastException;
+                            public void c() throws IndexOutOfBoundsException;
+                            public void d() throws UndeclaredThrowableException;
+                            public void e() throws MalformedParametersException;
+                            public void f() throws MalformedParameterizedTypeException;
+                            public void g() throws WrongMethodTypeException;
+                            public void h() throws EnumConstantNotPresentException;
+                            public void i() throws IllegalMonitorStateException;
+                            public void j() throws SecurityException;
+                            public void k() throws UnsupportedOperationException;
+                            public void l() throws AnnotationTypeMismatchException;
+                            public void m() throws IncompleteAnnotationException;
+                            public void n() throws TypeNotPresentException;
+                            public void o() throws IllegalStateException;
+                            public void p() throws ArithmeticException;
+                            public void q() throws IllegalArgumentException;
+                            public void r() throws ArrayStoreException;
+                            public void s() throws NegativeArraySizeException;
+                            public void t() throws MissingResourceException;
+                            public void u() throws EmptyStackException;
+                            public void v() throws CompletionException;
+                            public void w() throws RejectedExecutionException;
+                            public void x() throws IllformedLocaleException;
+                            public void y() throws ConcurrentModificationException;
+                            public void z() throws NoSuchElementException;
+                            public void aa() throws UncheckedIOException;
+                            public void ab() throws DateTimeException;
+                            public void ac() throws ProviderException;
+                            public void ad() throws BufferUnderflowException;
+                            public void ae() throws BufferOverflowException;
+                            public void af() throws AssertionError;
+                        }
+                    """
+                    ),
+                )
+        )
+    }
+
+    @Test
+    fun `Test throws type parameter`() {
+        check(
+            apiLint = "", // enabled
+            expectedIssues =
+                """
+                src/test/pkg/Test.java:9: error: Methods must not throw unchecked exceptions [BannedThrow]
+                """,
+            expectedFail = DefaultLintErrorMessage,
+            sourceFiles =
+                arrayOf(
+                    java(
+                        """
+                            package test.pkg;
+
+                            @SuppressWarnings("ALL")
+                            public final class Test {
+                                private Test() {}
+                                public <X extends Throwable> void throwsTypeParameter() throws X {
+                                    return null;
+                                }
+                                public <X extends IllegalStateException> void throwsUncheckedTypeParameter() throws X {
+                                    return null;
+                                }
+                            }
+                        """
+                    ),
+                )
+        )
+    }
+}