Add XTypeParameterElement to XProcessing

This CL adds an XTypeParameterElement to XProcessing and makes the
element available through the XParameterizable interface which is
extended by both XTypeElement and XExecutableElement. This mimics the
javac model for type parameters.

Test: XTypeParameterElementTest
Change-Id: Ia2ff9a1c4a685cc5e484511206ce33b5848e613c
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XExecutableElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XExecutableElement.kt
index 87001af..532cfcb 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XExecutableElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XExecutableElement.kt
@@ -21,7 +21,7 @@
  *
  * @see [javax.lang.model.element.ExecutableElement]
  */
-interface XExecutableElement : XHasModifiers, XElement {
+interface XExecutableElement : XHasModifiers, XParameterizable, XElement {
     /**
      * The element that declared this executable.
      *
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XParameterizable.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XParameterizable.kt
new file mode 100644
index 0000000..06d653f
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XParameterizable.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 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 androidx.room.compiler.processing
+
+/**
+ * Common interface for elements which might have type parameters (e.g. class, interface, method,
+ * or constructor)
+ */
+interface XParameterizable {
+    /** The type parameters of this element. */
+    val typeParameters: List<XTypeParameterElement>
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XTypeElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XTypeElement.kt
index 7a16840..8627f6f 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XTypeElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XTypeElement.kt
@@ -18,7 +18,7 @@
 
 import com.squareup.javapoet.ClassName
 
-interface XTypeElement : XHasModifiers, XElement, XMemberContainer {
+interface XTypeElement : XHasModifiers, XParameterizable, XElement, XMemberContainer {
     /**
      * The qualified name of the Class/Interface.
      */
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XTypeParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XTypeParameterElement.kt
new file mode 100644
index 0000000..9d43c5f
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XTypeParameterElement.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2022 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 androidx.room.compiler.processing
+
+import com.squareup.javapoet.TypeVariableName
+
+interface XTypeParameterElement : XElement {
+    /** Returns the simple name of this type parameter. */
+    val name: String
+
+    /**
+     * Returns the generic class, interface, or method that is parameterized by this type parameter.
+     */
+    override val enclosingElement: XElement
+
+    /**
+     *  Returns the bounds of this type parameter.
+     *
+     *  Note: If there are no explicit bounds, then this list contains a single type representing
+     *  `java.lang.Object` in Javac or `kotlin.Any?` in KSP.
+     */
+    val bounds: List<XType>
+
+    /** Returns the [TypeVariableName] for this type parameter) */
+    val typeVariableName: TypeVariableName
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt
index b986943..d68d18d 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt
@@ -19,6 +19,7 @@
 import androidx.room.compiler.processing.XConstructorElement
 import androidx.room.compiler.processing.XConstructorType
 import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.XTypeParameterElement
 import androidx.room.compiler.processing.javac.kotlin.KmConstructor
 import com.google.auto.common.MoreTypes
 import javax.lang.model.element.ElementKind
@@ -40,6 +41,14 @@
         }
     }
 
+    override val typeParameters: List<XTypeParameterElement> by lazy {
+        element.typeParameters.map {
+            // Type parameters are not allowed in Kotlin sources, so if type parameters exist they
+            // must have come from Java sources. Thus, there's no kotlin metadata so just use null.
+            JavacTypeParameterElement(env, this, it, null)
+        }
+    }
+
     override val parameters: List<JavacMethodParameter> by lazy {
         element.parameters.mapIndexed { index, variable ->
             JavacMethodParameter(
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt
index 167315a..c06ca00 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt
@@ -21,6 +21,7 @@
 import androidx.room.compiler.processing.XProcessingEnv
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeElement
+import androidx.room.compiler.processing.XTypeParameterElement
 import androidx.room.compiler.processing.javac.kotlin.KmFunction
 import com.google.auto.common.MoreElements
 import com.google.auto.common.MoreTypes
@@ -53,6 +54,13 @@
     override val jvmName: String
         get() = element.simpleName.toString()
 
+    override val typeParameters: List<XTypeParameterElement> by lazy {
+        element.typeParameters.mapIndexed { index, typeParameter ->
+            val typeArgument = kotlinMetadata?.typeArguments?.get(index)
+            JavacTypeParameterElement(env, this, typeParameter, typeArgument)
+        }
+    }
+
     override val parameters: List<JavacMethodParameter> by lazy {
         element.parameters.mapIndexed { index, variable ->
             JavacMethodParameter(
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
index 9a5fa0a..4911f16 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
@@ -24,6 +24,7 @@
 import androidx.room.compiler.processing.XMemberContainer
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeElement
+import androidx.room.compiler.processing.XTypeParameterElement
 import androidx.room.compiler.processing.collectAllMethods
 import androidx.room.compiler.processing.collectFieldsIncludingPrivateSupers
 import androidx.room.compiler.processing.filterMethodsByConfig
@@ -66,6 +67,13 @@
         enclosingTypeElement
     }
 
+    override val typeParameters: List<XTypeParameterElement> by lazy {
+        element.typeParameters.mapIndexed { index, typeParameter ->
+            val typeArgument = kotlinMetadata?.kmType?.typeArguments?.get(index)
+            JavacTypeParameterElement(env, this, typeParameter, typeArgument)
+        }
+    }
+
     override val closestMemberContainer: JavacTypeElement
         get() = this
 
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeParameterElement.kt
new file mode 100644
index 0000000..42d85fb
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeParameterElement.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2022 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 androidx.room.compiler.processing.javac
+
+import androidx.room.compiler.processing.XElement
+import androidx.room.compiler.processing.XMemberContainer
+import androidx.room.compiler.processing.XNullability
+import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.XTypeParameterElement
+import androidx.room.compiler.processing.javac.kotlin.KmType
+import com.squareup.javapoet.TypeVariableName
+import javax.lang.model.element.TypeParameterElement
+
+internal class JavacTypeParameterElement(
+    env: JavacProcessingEnv,
+    override val enclosingElement: XElement,
+    override val element: TypeParameterElement,
+    private val kotlinType: KmType?,
+) : JavacElement(env, element), XTypeParameterElement {
+    override val name: String
+        get() = element.simpleName.toString()
+
+    override val typeVariableName: TypeVariableName by lazy {
+        TypeVariableName.get(name, *bounds.map { it.typeName }.toTypedArray())
+    }
+
+    override val bounds: List<XType> by lazy {
+        element.bounds.map { env.wrap(it, kotlinType?.extendsBound, XNullability.UNKNOWN) }
+    }
+
+    override val fallbackLocationText: String
+        get() = element.simpleName.toString()
+
+    override val closestMemberContainer: XMemberContainer
+        get() = enclosingElement.closestMemberContainer
+
+    override val equalityItems: Array<out Any?>
+        get() = arrayOf(element)
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt
index ec895aa..2ab2dd4 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt
@@ -61,6 +61,7 @@
     val name: String,
     val descriptor: String,
     private val flags: Flags,
+    val typeArguments: List<KmType>,
     override val parameters: List<KmValueParameter>,
     val returnType: KmType,
     val receiverType: KmType?
@@ -140,10 +141,22 @@
         return object : KmFunctionVisitor() {
 
             lateinit var methodSignature: JvmMethodSignature
+            private val typeParameters = mutableListOf<KmTypeParameter>()
             val parameters = mutableListOf<KmValueParameter>()
             lateinit var returnType: KmType
             var receiverType: KmType? = null
 
+            override fun visitTypeParameter(
+                flags: Flags,
+                name: String,
+                id: Int,
+                variance: KmVariance
+            ): KmTypeParameterVisitor {
+                return TypeParameterReader(name, flags) {
+                    typeParameters.add(it)
+                }
+            }
+
             override fun visitValueParameter(
                 flags: Flags,
                 name: String
@@ -183,6 +196,7 @@
                         jvmName = methodSignature.name,
                         descriptor = methodSignature.asString(),
                         flags = flags,
+                        typeArguments = typeParameters.map { it.asKmType() },
                         parameters = parameters,
                         returnType = returnType,
                         receiverType = receiverType
@@ -302,6 +316,7 @@
                                 name = JvmAbi.computeSetterName(name),
                                 descriptor = setterSignature.asString(),
                                 flags = 0,
+                                typeArguments = emptyList(),
                                 parameters = listOf(param),
                                 returnType = KM_VOID_TYPE,
                                 receiverType = null
@@ -313,6 +328,7 @@
                                 name = JvmAbi.computeGetterName(name),
                                 descriptor = getterSignature.asString(),
                                 flags = flags,
+                                typeArguments = emptyList(),
                                 parameters = emptyList(),
                                 returnType = returnType,
                                 receiverType = null
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
index b339a2e..ac41dfe 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
@@ -21,6 +21,7 @@
 import androidx.room.compiler.processing.XHasModifiers
 import androidx.room.compiler.processing.XMemberContainer
 import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.XTypeParameterElement
 import androidx.room.compiler.processing.ksp.KspAnnotated.UseSiteFilter.Companion.NO_USE_SITE
 import androidx.room.compiler.processing.util.ISSUE_TRACKER_LINK
 import com.google.devtools.ksp.KspExperimental
@@ -55,6 +56,10 @@
     override val closestMemberContainer: XMemberContainer
         get() = enclosingElement
 
+    override val typeParameters: List<XTypeParameterElement> by lazy {
+        declaration.typeParameters.map { KspTypeParameterElement(env, it) }
+    }
+
     @OptIn(KspExperimental::class)
     override val thrownTypes: List<XType> by lazy {
         env.resolver.getJvmCheckedException(declaration).map {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
index 4133789..94d35a7 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
@@ -26,6 +26,7 @@
 import androidx.room.compiler.processing.XMethodElement
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeElement
+import androidx.room.compiler.processing.XTypeParameterElement
 import androidx.room.compiler.processing.collectAllMethods
 import androidx.room.compiler.processing.collectFieldsIncludingPrivateSupers
 import androidx.room.compiler.processing.filterMethodsByConfig
@@ -69,6 +70,10 @@
     override val enclosingElement: XMemberContainer?
         get() = enclosingTypeElement
 
+    override val typeParameters: List<XTypeParameterElement> by lazy {
+        declaration.typeParameters.map { KspTypeParameterElement(env, it) }
+    }
+
     override val equalityItems: Array<out Any?> by lazy {
         arrayOf(declaration)
     }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeParameterElement.kt
new file mode 100644
index 0000000..8d9f24d6
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeParameterElement.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022 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 androidx.room.compiler.processing.ksp
+
+import androidx.room.compiler.processing.XAnnotated
+import androidx.room.compiler.processing.XMemberContainer
+import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.XTypeParameterElement
+import androidx.room.compiler.processing.ksp.KspAnnotated.UseSiteFilter.Companion.NO_USE_SITE_OR_FIELD
+import com.google.devtools.ksp.symbol.KSTypeParameter
+import com.squareup.javapoet.TypeVariableName
+
+internal class KspTypeParameterElement(
+    env: KspProcessingEnv,
+    override val declaration: KSTypeParameter
+) : KspElement(env, declaration),
+    XTypeParameterElement,
+    XAnnotated by KspAnnotated.create(env, declaration, NO_USE_SITE_OR_FIELD) {
+
+    override val name: String
+        get() = declaration.name.asString()
+
+    override val typeVariableName: TypeVariableName by lazy {
+        TypeVariableName.get(name, *bounds.map { it.typeName }.toTypedArray())
+    }
+
+    override val enclosingElement: KspMemberContainer by lazy {
+        declaration.requireEnclosingMemberContainer(env)
+    }
+
+    override val bounds: List<XType> by lazy {
+        declaration.bounds.map { env.wrap(it, it.resolve()) }.toList().ifEmpty {
+            listOf(env.requireType(Any::class).makeNullable())
+        }
+    }
+
+    override val fallbackLocationText: String
+        get() = "${declaration.name} in ${enclosingElement.fallbackLocationText}"
+
+    override val closestMemberContainer: XMemberContainer
+        get() = enclosingElement.closestMemberContainer
+
+    override val equalityItems: Array<out Any?>
+        get() = arrayOf(declaration)
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
index 39a7931..3b2dbd0 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
@@ -25,6 +25,7 @@
 import androidx.room.compiler.processing.XMethodType
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeElement
+import androidx.room.compiler.processing.XTypeParameterElement
 import androidx.room.compiler.processing.javac.kotlin.JvmAbi
 import androidx.room.compiler.processing.ksp.KspAnnotated
 import androidx.room.compiler.processing.ksp.KspAnnotated.UseSiteFilter.Companion.NO_USE_SITE_OR_GETTER
@@ -166,6 +167,9 @@
             field.type
         }
 
+        override val typeParameters: List<XTypeParameterElement>
+            get() = emptyList()
+
         override val parameters: List<XExecutableParameterElement>
             get() = emptyList()
 
@@ -196,6 +200,9 @@
             env.voidType
         }
 
+        override val typeParameters: List<XTypeParameterElement>
+            get() = emptyList()
+
         override val parameters: List<XExecutableParameterElement> by lazy {
             listOf(
                 SyntheticExecutableParameterElement(
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeParameterElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeParameterElementTest.kt
new file mode 100644
index 0000000..2b63704
--- /dev/null
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeParameterElementTest.kt
@@ -0,0 +1,357 @@
+/*
+ * Copyright 2022 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 androidx.room.compiler.processing
+
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.runProcessorTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class XTypeParameterElementTest {
+
+    @Test
+    fun classTypeParameters() {
+        val src = Source.kotlin(
+            "Foo.kt",
+            """
+            class Foo<T1, T2>
+            """.trimIndent()
+        )
+        runProcessorTest(sources = listOf(src)) { invocation ->
+            val foo = invocation.processingEnv.requireTypeElement("Foo")
+
+            assertThat(foo.typeParameters).hasSize(2)
+            val t1 = foo.typeParameters[0]
+            val t2 = foo.typeParameters[1]
+            assertThat(t1.name).isEqualTo("T1")
+            assertThat(t1.typeVariableName.name).isEqualTo("T1")
+            assertThat(t1.typeVariableName.bounds).isEmpty()
+            assertThat(t2.name).isEqualTo("T2")
+            assertThat(t2.typeVariableName.name).isEqualTo("T2")
+            assertThat(t2.typeVariableName.bounds).isEmpty()
+
+            // Note: Javac and KSP have different default types when no bounds are provided.
+            val expectedBoundType = if (invocation.isKsp) {
+                invocation.processingEnv.requireType("kotlin.Any").makeNullable()
+            } else {
+                invocation.processingEnv.requireType("java.lang.Object")
+            }
+
+            assertThat(t1.bounds).hasSize(1)
+            val t1Bound = t1.bounds[0]
+            assertThat(t1Bound.typeName.toString()).isEqualTo("java.lang.Object")
+            assertThat(t1Bound.isSameType(expectedBoundType)).isTrue()
+
+            assertThat(t2.bounds).hasSize(1)
+            val t2Bound = t2.bounds[0]
+            assertThat(t2Bound.typeName.toString()).isEqualTo("java.lang.Object")
+            assertThat(t2Bound.isSameType(expectedBoundType)).isTrue()
+        }
+    }
+
+    @Test
+    fun classTypeParametersWithBounds() {
+        val src = Source.kotlin(
+            "Foo.kt",
+            """
+            class Foo<T1 : Bar, T2 : Baz?>
+            open class Bar
+            open class Baz
+            """.trimIndent()
+        )
+        runProcessorTest(sources = listOf(src)) { invocation ->
+            val foo = invocation.processingEnv.requireTypeElement("Foo")
+
+            assertThat(foo.typeParameters).hasSize(2)
+            val t1 = foo.typeParameters[0]
+            val t2 = foo.typeParameters[1]
+            assertThat(t1.name).isEqualTo("T1")
+            assertThat(t1.typeVariableName.name).isEqualTo("T1")
+            assertThat(t1.typeVariableName.bounds.map { it.toString() }).containsExactly("Bar")
+            assertThat(t2.name).isEqualTo("T2")
+            assertThat(t2.typeVariableName.name).isEqualTo("T2")
+            assertThat(t2.typeVariableName.bounds.map { it.toString() }).containsExactly("Baz")
+
+            assertThat(t1.bounds).hasSize(1)
+            val t1Bound = t1.bounds[0]
+            assertThat(t1Bound.typeName.toString()).isEqualTo("Bar")
+            val bar = invocation.processingEnv.requireType("Bar")
+            assertThat(t1Bound.isSameType(bar)).isTrue()
+
+            assertThat(t2.bounds).hasSize(1)
+            val t2Bound = t2.bounds[0]
+            assertThat(t2Bound.typeName.toString()).isEqualTo("Baz")
+            val nullableBar = invocation.processingEnv.requireType("Baz").makeNullable()
+            assertThat(t2Bound.isSameType(nullableBar)).isTrue()
+        }
+    }
+
+    @Test
+    fun classTypeParametersWithInOut() {
+        val src = Source.kotlin(
+            "Foo.kt",
+            """
+            class Foo<in T1 : Bar?, out T2 : Baz>
+            open class Bar
+            open class Baz
+            """.trimIndent()
+        )
+        runProcessorTest(sources = listOf(src)) { invocation ->
+            val foo = invocation.processingEnv.requireTypeElement("Foo")
+
+            assertThat(foo.typeParameters).hasSize(2)
+            val t1 = foo.typeParameters[0]
+            val t2 = foo.typeParameters[1]
+            assertThat(t1.name).isEqualTo("T1")
+            assertThat(t1.typeVariableName.name).isEqualTo("T1")
+            assertThat(t1.typeVariableName.bounds.map { it.toString() }).containsExactly("Bar")
+            assertThat(t2.name).isEqualTo("T2")
+            assertThat(t2.typeVariableName.name).isEqualTo("T2")
+            assertThat(t2.typeVariableName.bounds.map { it.toString() }).containsExactly("Baz")
+
+            assertThat(t1.bounds).hasSize(1)
+            val t1Bound = t1.bounds[0]
+            assertThat(t1Bound.typeName.toString()).isEqualTo("Bar")
+            val nullableBar = invocation.processingEnv.requireType("Bar").makeNullable()
+            assertThat(t1Bound.isSameType(nullableBar)).isTrue()
+
+            assertThat(t2.bounds).hasSize(1)
+            val t2Bound = t2.bounds[0]
+            assertThat(t2Bound.typeName.toString()).isEqualTo("Baz")
+            val baz = invocation.processingEnv.requireType("Baz")
+            assertThat(t2Bound.isSameType(baz)).isTrue()
+        }
+    }
+
+    @Test
+    fun javaClassTypeParametersWithExtends() {
+        val src = Source.java(
+            "Foo",
+            """
+            class Foo<T extends Bar & Baz> {}
+            class Bar {}
+            interface Baz {}
+            """.trimIndent()
+        )
+        runProcessorTest(sources = listOf(src)) { invocation ->
+            val foo = invocation.processingEnv.requireTypeElement("Foo")
+
+            assertThat(foo.typeParameters).hasSize(1)
+            val t = foo.typeParameters[0]
+            assertThat(t.name).isEqualTo("T")
+            assertThat(t.typeVariableName.name).isEqualTo("T")
+            assertThat(t.typeVariableName.bounds.map { it.toString() })
+                .containsExactly("Bar", "Baz")
+            assertThat(t.bounds).hasSize(2)
+
+            val bound0 = t.bounds[0]
+            assertThat(bound0.typeName.toString()).isEqualTo("Bar")
+            val bar = invocation.processingEnv.requireType("Bar")
+            assertThat(bound0.isSameType(bar)).isTrue()
+            assertThat(bound0.nullability).isEqualTo(XNullability.UNKNOWN)
+
+            val bound1 = t.bounds[1]
+            assertThat(bound1.typeName.toString()).isEqualTo("Baz")
+            val baz = invocation.processingEnv.requireType("Baz")
+            assertThat(bound1.isSameType(baz)).isTrue()
+            assertThat(bound1.nullability).isEqualTo(XNullability.UNKNOWN)
+        }
+    }
+
+    @Test
+    fun methodTypeParameters() {
+        val src = Source.kotlin(
+            "Foo.kt",
+            """
+            class Foo {
+              fun <T1, T2> someMethod(t1: T1, t2: T2) {}
+            }
+            """.trimIndent()
+        )
+        runProcessorTest(sources = listOf(src)) { invocation ->
+            val foo = invocation.processingEnv.requireTypeElement("Foo")
+            val methods = foo.getDeclaredMethods()
+            assertThat(methods).hasSize(1)
+
+            val method = methods[0]
+            assertThat(method.jvmName).isEqualTo("someMethod")
+            assertThat(method.typeParameters).hasSize(2)
+
+            val t1 = method.typeParameters[0]
+            val t2 = method.typeParameters[1]
+            assertThat(t1.name).isEqualTo("T1")
+            assertThat(t1.typeVariableName.name).isEqualTo("T1")
+            assertThat(t1.typeVariableName.bounds).isEmpty()
+            assertThat(t2.name).isEqualTo("T2")
+            assertThat(t2.typeVariableName.name).isEqualTo("T2")
+            assertThat(t2.typeVariableName.bounds).isEmpty()
+
+            // Note: Javac and KSP have different default types when no bounds are provided.
+            val expectedBoundType = if (invocation.isKsp) {
+                invocation.processingEnv.requireType("kotlin.Any").makeNullable()
+            } else {
+                invocation.processingEnv.requireType("java.lang.Object")
+            }
+
+            assertThat(t1.bounds).hasSize(1)
+            val t1Bound = t1.bounds[0]
+            assertThat(t1Bound.typeName.toString()).isEqualTo("java.lang.Object")
+            assertThat(t1Bound.isSameType(expectedBoundType)).isTrue()
+
+            assertThat(t2.bounds).hasSize(1)
+            val t2Bound = t2.bounds[0]
+            assertThat(t2Bound.typeName.toString()).isEqualTo("java.lang.Object")
+            assertThat(t2Bound.isSameType(expectedBoundType)).isTrue()
+        }
+    }
+
+    @Test
+    fun methodTypeParametersWithBounds() {
+        val src = Source.kotlin(
+            "Foo.kt",
+            """
+            class Foo {
+              fun <T1 : Bar, T2 : Baz?> someMethod(t1: T1, t2: T2) {}
+            }
+            open class Bar
+            open class Baz
+            """.trimIndent()
+        )
+        runProcessorTest(sources = listOf(src)) { invocation ->
+            val foo = invocation.processingEnv.requireTypeElement("Foo")
+            val methods = foo.getDeclaredMethods()
+            assertThat(methods).hasSize(1)
+
+            val method = methods[0]
+            assertThat(method.jvmName).isEqualTo("someMethod")
+            assertThat(method.typeParameters).hasSize(2)
+
+            val t1 = method.typeParameters[0]
+            val t2 = method.typeParameters[1]
+            assertThat(t1.name).isEqualTo("T1")
+            assertThat(t1.typeVariableName.name).isEqualTo("T1")
+            assertThat(t1.typeVariableName.bounds.map { it.toString() }).containsExactly("Bar")
+            assertThat(t2.name).isEqualTo("T2")
+            assertThat(t2.typeVariableName.name).isEqualTo("T2")
+            assertThat(t2.typeVariableName.bounds.map { it.toString() }).containsExactly("Baz")
+
+            assertThat(t1.bounds).hasSize(1)
+            val t1Bound = t1.bounds[0]
+            assertThat(t1Bound.typeName.toString()).isEqualTo("Bar")
+            val bar = invocation.processingEnv.requireType("Bar")
+            assertThat(t1Bound.isSameType(bar)).isTrue()
+
+            assertThat(t2.bounds).hasSize(1)
+            val t2Bound = t2.bounds[0]
+            assertThat(t2Bound.typeName.toString()).isEqualTo("Baz")
+            val nullableBar = invocation.processingEnv.requireType("Baz").makeNullable()
+            assertThat(t2Bound.isSameType(nullableBar)).isTrue()
+        }
+    }
+
+    @Test
+    fun javaMethodTypeParametersWithExtends() {
+        val src = Source.java(
+            "Foo",
+            """
+            class Foo {
+              <T extends Bar & Baz> void someMethod(T t) {}
+            }
+            class Bar {}
+            interface Baz {}
+            """.trimIndent()
+        )
+        runProcessorTest(sources = listOf(src)) { invocation ->
+            val foo = invocation.processingEnv.requireTypeElement("Foo")
+            val methods = foo.getDeclaredMethods()
+            assertThat(methods).hasSize(1)
+
+            val method = methods[0]
+            assertThat(method.jvmName).isEqualTo("someMethod")
+            assertThat(method.typeParameters).hasSize(1)
+
+            val t = method.typeParameters[0]
+            assertThat(t.name).isEqualTo("T")
+            assertThat(t.typeVariableName.name).isEqualTo("T")
+            assertThat(t.typeVariableName.bounds.map { it.toString() })
+                .containsExactly("Bar", "Baz")
+            assertThat(t.bounds).hasSize(2)
+
+            val bound0 = t.bounds[0]
+            assertThat(bound0.typeName.toString()).isEqualTo("Bar")
+            val bar = invocation.processingEnv.requireType("Bar")
+            assertThat(bound0.isSameType(bar)).isTrue()
+            assertThat(bound0.nullability).isEqualTo(XNullability.UNKNOWN)
+
+            val bound1 = t.bounds[1]
+            assertThat(bound1.typeName.toString()).isEqualTo("Baz")
+            val baz = invocation.processingEnv.requireType("Baz")
+            assertThat(bound1.isSameType(baz)).isTrue()
+            assertThat(bound1.nullability).isEqualTo(XNullability.UNKNOWN)
+        }
+    }
+
+    // Note: constructor type parameters are only allowed in Java sources.
+    @Test
+    fun javaConstructorTypeParametersWithExtends() {
+        val src = Source.java(
+            "Foo",
+            """
+            class Foo {
+              <T extends Bar & Baz> Foo(T t) {}
+            }
+            class Bar {}
+            interface Baz {}
+            """.trimIndent()
+        )
+        runProcessorTest(sources = listOf(src)) { invocation ->
+            val foo = invocation.processingEnv.requireTypeElement("Foo")
+            val constructors = foo.getConstructors()
+            assertThat(constructors).hasSize(1)
+
+            val constructor = constructors[0]
+            assertThat(constructor.typeParameters).hasSize(1)
+
+            val t = constructor.typeParameters[0]
+            assertThat(t.name).isEqualTo("T")
+            assertThat(t.typeVariableName.name).isEqualTo("T")
+            assertThat(t.typeVariableName.bounds.map { it.toString() })
+                .containsExactly("Bar", "Baz")
+            assertThat(t.bounds).hasSize(2)
+
+            val bound0 = t.bounds[0]
+            assertThat(bound0.typeName.toString()).isEqualTo("Bar")
+            val bar = invocation.processingEnv.requireType("Bar")
+            assertThat(bound0.isSameType(bar)).isTrue()
+            assertThat(bound0.nullability).isEqualTo(XNullability.UNKNOWN)
+
+            val bound1 = t.bounds[1]
+            assertThat(bound1.typeName.toString()).isEqualTo("Baz")
+            val baz = invocation.processingEnv.requireType("Baz")
+            assertThat(bound1.isSameType(baz)).isTrue()
+            assertThat(bound1.nullability).isEqualTo(XNullability.UNKNOWN)
+
+            assertThat(constructor.parameters).hasSize(1)
+            val parameter = constructor.parameters[0]
+            assertThat(parameter.name).isEqualTo("t")
+            assertThat(parameter.type.typeName.toString()).isEqualTo("T")
+        }
+    }
+}