[GH] [Room][Compiler Processing] Add additional type element information

## Proposed Changes
Adds an interface function to get element kdoc. Implements this for javac, and uses null for KSP until https://github.com/google/ksp/issues/392 is fixed

Also surfaces more XTypeElement information, such as whether the type is a data class, a fun interface, a companion object, etc.

I implemented everything exposed in kotlin class metadata that seemed reasonable in order to future proof it a bit.

I was not able to implement fun interface checking for KSP since it is not yet surfaced in KSP - https://github.com/google/ksp/issues/393

## Testing

Test: Added cases to XTypeElementTest.modifiers

Note, I wasn't able to test the `expect` modifier. First I had to add support for custom compiler arguments so I could enable multiplatform via `"-Xmulti-platform"` but to get the test sources to compile I had to include a `actual` implementation as well, and since both the expected and actual implementations were in the same sources it seems the processor only picks up the actual. Omitting "actual" implementation doesn't work as compilation fails with "no actual implementation found in module", and I'm not sure how to have the compiler processing testing work with multiple module sources. The implementation of the "expect" modifier is very simple so I'm not sure how important the test is.

## Issues Fixed

Fixes: https://issuetracker.google.com/issues/185672887

This is an imported pull request from https://github.com/androidx/androidx/pull/164.

Resolves #164
Github-Pr-Head-Sha: 0bcd0afc8f42c8fb0b7a94a965fe665423e45df1
GitOrigin-RevId: 04f408a71641bc716f10cac4b7b609f7b4e1f501
Change-Id: Ifcf6b7aac73643866034e4bb2e95df81ee997b72
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XElement.kt
index b06d8c9..497b194 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XElement.kt
@@ -40,6 +40,11 @@
      * message. Without this information, developer gets no clue on where the error is.
      */
     val fallbackLocationText: String
+
+    /**
+     * The documentation comment of the element, or null if there is none.
+     */
+    val docComment: String?
 }
 
 /**
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XMessager.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XMessager.kt
index 16f1bba..65561ab 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XMessager.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XMessager.kt
@@ -37,7 +37,11 @@
         onPrintMessage(kind, msg, element)
     }
 
-    abstract fun onPrintMessage(kind: Diagnostic.Kind, msg: String, element: XElement? = null)
+    protected abstract fun onPrintMessage(
+        kind: Diagnostic.Kind,
+        msg: String,
+        element: XElement? = null
+    )
 
     fun addMessageWatcher(watcher: XMessager) {
         watchers.add(watcher)
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XTypeElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XTypeElement.kt
index 5109aff..aa692e1 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XTypeElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XTypeElement.kt
@@ -65,11 +65,49 @@
     fun isInterface(): Boolean
 
     /**
-     * Returns `true` if this [XTypeElement] is declared as a Kotlin `object`
+     * Returns `true` if this [XTypeElement] represents a Kotlin functional interface,
+     * i.e. marked with the keyword `fun`
+     */
+    fun isFunctionalInterface(): Boolean
+
+    /**
+     * Returns `true` if this [XTypeElement] represents an ordinary class. ie not an enum, object,
+     * annotation, interface, or other type of specialty class.
+     */
+    fun isClass(): Boolean
+
+    /**
+     * Returns `true` if this [XTypeElement] represents a Kotlin data class
+     */
+    fun isDataClass(): Boolean
+
+    /**
+     * Returns `true` if this [XTypeElement] represents a Kotlin value class
+     */
+    fun isValueClass(): Boolean
+
+    /**
+     * Returns `true` if this [XTypeElement] represents a class with the Kotlin `expect` keyword
+     */
+    fun isExpect(): Boolean
+
+    /**
+     * Returns `true` if this [XTypeElement] represents a Kotlin annotation class or a Java
+     * annotation type.
+     */
+    fun isAnnotationClass(): Boolean
+
+    /**
+     * Returns `true` if this [XTypeElement] is a non-companion `object` in Kotlin
      */
     fun isKotlinObject(): Boolean
 
     /**
+     * Returns `true` if this [XTypeElement] is declared as a Kotlin `companion object`
+     */
+    fun isCompanionObject(): Boolean
+
+    /**
      * All fields, including private supers.
      * Room only ever reads fields this way.
      */
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
index 47eb36b..497a809 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
@@ -86,4 +86,8 @@
             MoreElements.getPackage(it.annotationType.asElement()).toString() == pkg
         }
     }
+
+    override val docComment: String? by lazy {
+        env.elementUtils.getDocComment(element)
+    }
 }
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
index acaffb7..1266ef0 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
@@ -21,7 +21,6 @@
 import androidx.room.compiler.processing.XHasModifiers
 import androidx.room.compiler.processing.XMethodElement
 import androidx.room.compiler.processing.XTypeElement
-import androidx.room.compiler.processing.javac.JavacTypeElement.JavacEnumTypeElement
 import androidx.room.compiler.processing.javac.kotlin.KotlinMetadataElement
 import com.google.auto.common.MoreElements
 import com.google.auto.common.MoreTypes
@@ -58,8 +57,6 @@
         element.enclosingType(env)
     }
 
-    override fun isInterface() = element.kind == ElementKind.INTERFACE
-
     private val _allFieldsIncludingPrivateSupers by lazy {
         element.getAllFieldsIncludingPrivateSupers(
             env.elementUtils
@@ -77,6 +74,24 @@
     }
 
     override fun isKotlinObject() = kotlinMetadata?.isObject() == true
+    override fun isCompanionObject() = kotlinMetadata?.isCompanionObject() == true
+    override fun isDataClass() = kotlinMetadata?.isDataClass() == true
+    override fun isValueClass() = kotlinMetadata?.isValueClass() == true
+    override fun isFunctionalInterface() = kotlinMetadata?.isFunctionalInterface() == true
+    override fun isExpect() = kotlinMetadata?.isExpect() == true
+
+    override fun isAnnotationClass(): Boolean {
+        return kotlinMetadata?.isAnnotationClass()
+            ?: (element.kind == ElementKind.ANNOTATION_TYPE)
+    }
+
+    override fun isClass(): Boolean {
+        return kotlinMetadata?.isClass() ?: (element.kind == ElementKind.CLASS)
+    }
+
+    override fun isInterface(): Boolean {
+        return kotlinMetadata?.isInterface() ?: (element.kind == ElementKind.INTERFACE)
+    }
 
     override fun findPrimaryConstructor(): JavacConstructorElement? {
         val primarySignature = kotlinMetadata?.findPrimaryConstructorSignature() ?: return null
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt
index 2ae8d57..2b3fc32 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt
@@ -39,6 +39,7 @@
 internal interface KmExecutable {
     val parameters: List<KmValueParameter>
 }
+
 /**
  * Represents the kotlin metadata of a function
  */
@@ -68,6 +69,7 @@
 ) {
     val typeParameters
         get() = type.typeArguments
+
     fun isNullable() = Flag.Type.IS_NULLABLE(type.flags)
 }
 
@@ -188,25 +190,42 @@
     }
 }
 
-internal fun KotlinClassMetadata.Class.isObject(): Boolean = ObjectReader().let {
-    this@isObject.accept(it)
-    it.isObject
+internal class KotlinMetadataClassFlags(val classMetadata: KotlinClassMetadata.Class) {
+
+    private val flags: Flags by lazy {
+        var theFlags: Flags = 0
+        classMetadata.accept(object : KmClassVisitor() {
+            override fun visit(flags: Flags, name: ClassName) {
+                theFlags = flags
+                super.visit(flags, name)
+            }
+        })
+        return@lazy theFlags
+    }
+
+    fun isObject(): Boolean = Flag.Class.IS_OBJECT(flags)
+
+    fun isCompanionObject(): Boolean = Flag.Class.IS_COMPANION_OBJECT(flags)
+
+    fun isAnnotationClass(): Boolean = Flag.Class.IS_ANNOTATION_CLASS(flags)
+
+    fun isInterface(): Boolean = Flag.Class.IS_INTERFACE(flags)
+
+    fun isClass(): Boolean = Flag.Class.IS_CLASS(flags)
+
+    fun isDataClass(): Boolean = Flag.Class.IS_DATA(flags)
+
+    fun isValueClass(): Boolean = Flag.Class.IS_INLINE(flags)
+
+    fun isFunctionalInterface(): Boolean = Flag.Class.IS_FUN(flags)
+
+    fun isExpect(): Boolean = Flag.Class.IS_EXPECT(flags)
 }
 
 internal fun KotlinClassMetadata.Class.readProperties(): List<KmProperty> =
     mutableListOf<KmProperty>().apply { accept(PropertyReader(this)) }
 
 /**
- * Reads whether the given class is a kotlin object
- */
-private class ObjectReader : KmClassVisitor() {
-    var isObject: Boolean = false
-    override fun visit(flags: Flags, name: ClassName) {
-        isObject = Flag.Class.IS_OBJECT(flags)
-    }
-}
-
-/**
  * Reads the properties of a class declaration
  */
 private class PropertyReader(
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElement.kt
index 963c20a..cf281c4 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElement.kt
@@ -45,6 +45,9 @@
     private val functionList: List<KmFunction> by lazy { classMetadata.readFunctions() }
     private val constructorList: List<KmConstructor> by lazy { classMetadata.readConstructors() }
     private val propertyList: List<KmProperty> by lazy { classMetadata.readProperties() }
+    private val classFlags: KotlinMetadataClassFlags by lazy {
+        KotlinMetadataClassFlags(classMetadata)
+    }
 
     private val ExecutableElement.descriptor: String
         get() = descriptor()
@@ -53,7 +56,15 @@
         it.isPrimary()
     }?.descriptor
 
-    fun isObject(): Boolean = classMetadata.isObject()
+    fun isObject(): Boolean = classFlags.isObject()
+    fun isCompanionObject(): Boolean = classFlags.isCompanionObject()
+    fun isAnnotationClass(): Boolean = classFlags.isAnnotationClass()
+    fun isClass(): Boolean = classFlags.isClass()
+    fun isInterface(): Boolean = classFlags.isInterface()
+    fun isDataClass(): Boolean = classFlags.isDataClass()
+    fun isValueClass(): Boolean = classFlags.isValueClass()
+    fun isFunctionalInterface(): Boolean = classFlags.isFunctionalInterface()
+    fun isExpect(): Boolean = classFlags.isExpect()
 
     fun getFunctionMetadata(method: ExecutableElement): KmFunction? {
         check(method.kind == ElementKind.METHOD) {
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspElement.kt
index 22fa748..37ef859 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspElement.kt
@@ -61,4 +61,10 @@
             KSFileAsOriginatingElement(it)
         }
     }
+
+    override val docComment: String? by lazy {
+        // TODO: Not yet implemented in KSP.
+        // https://github.com/google/ksp/issues/392
+        null
+    }
 }
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFileMemberContainer.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFileMemberContainer.kt
index 9d91bb4..60df4da 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFileMemberContainer.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFileMemberContainer.kt
@@ -58,6 +58,12 @@
 
     override val fallbackLocationText: String = ksFile.filePath
 
+    override val docComment: String? by lazy {
+        // TODO: Not yet implemented in KSP.
+        // https://github.com/google/ksp/issues/392
+        null
+    }
+
     companion object {
         private fun KSFile.findClassName(): String {
             return annotations.firstOrNull {
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
index c7f17c6..404c5f5 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
@@ -40,6 +40,7 @@
 import com.google.devtools.ksp.symbol.Modifier
 import com.google.devtools.ksp.symbol.Origin
 import com.squareup.javapoet.ClassName
+import javax.tools.Diagnostic
 
 internal sealed class KspTypeElement(
     env: KspProcessingEnv,
@@ -253,6 +254,40 @@
         return declaration.classKind == ClassKind.OBJECT
     }
 
+    override fun isCompanionObject(): Boolean {
+        return declaration.isCompanionObject
+    }
+
+    override fun isAnnotationClass(): Boolean {
+        return declaration.classKind == ClassKind.ANNOTATION_CLASS
+    }
+
+    override fun isClass(): Boolean {
+        return declaration.classKind == ClassKind.CLASS
+    }
+
+    override fun isDataClass(): Boolean {
+        return Modifier.DATA in declaration.modifiers
+    }
+
+    override fun isValueClass(): Boolean {
+        return Modifier.INLINE in declaration.modifiers
+    }
+
+    override fun isFunctionalInterface(): Boolean {
+        // TODO: Update this once KSP supports it
+        // https://github.com/google/ksp/issues/393
+        env.messager.printMessage(
+            Diagnostic.Kind.WARNING,
+            "XProcessing does not yet support checking for functional interfaces in KSP."
+        )
+        return false
+    }
+
+    override fun isExpect(): Boolean {
+        return Modifier.EXPECT in declaration.modifiers
+    }
+
     override fun isFinal(): Boolean {
         // workaround for https://github.com/android/kotlin/issues/128
         return !isInterface() && !declaration.isOpen()
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
index aad60a8..cab0ab2 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
@@ -82,6 +82,9 @@
     override val fallbackLocationText: String
         get() = "return type of ${containing.fallbackLocationText}"
 
+    // Not applicable
+    override val docComment: String? get() = null
+
     override fun asMemberOf(other: XType): XType {
         check(other is KspType)
         val continuation = env.resolver.requireContinuationClass()
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
index 7ad376f..3c35af4 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
@@ -80,6 +80,12 @@
         )
     }
 
+    override val docComment: String? by lazy {
+        // Not yet implemented in KSP.
+        // https://github.com/google/ksp/issues/392
+        null
+    }
+
     final override fun asMemberOf(other: XType): XMethodType {
         return KspSyntheticPropertyMethodType.create(
             element = this,
@@ -226,6 +232,12 @@
                 return origin.field.asMemberOf(other)
             }
 
+            override val docComment: String? by lazy {
+                // Not yet implemented in KSP.
+                // https://github.com/google/ksp/issues/392
+                null
+            }
+
             override fun kindName(): String {
                 return "method parameter"
             }
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
index faded70..b8727eb 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
@@ -202,6 +202,12 @@
                 inner class InnerKotlinClass
                 class NestedKotlinClass
             }
+            annotation class KotlinAnnotation
+            data class DataClass(val foo: Int)
+            inline class InlineClass(val foo: Int)
+            fun interface FunInterface {
+               fun foo()
+            }
             """.trimIndent()
         )
         val javaSrc = Source.java(
@@ -213,8 +219,15 @@
             }
             """.trimIndent()
         )
+        val javaAnnotationSrc = Source.java(
+            "JavaAnnotation",
+            """
+            public @interface JavaAnnotation {
+            }
+            """.trimIndent()
+        )
         runProcessorTest(
-            sources = listOf(kotlinSrc, javaSrc)
+            sources = listOf(kotlinSrc, javaSrc, javaAnnotationSrc)
         ) { invocation ->
             fun getModifiers(element: XTypeElement): Set<String> {
                 val result = mutableSetOf<String>()
@@ -224,8 +237,15 @@
                 if (element.isProtected()) result.add("protected")
                 if (element.isPublic()) result.add("public")
                 if (element.isKotlinObject()) result.add("object")
+                if (element.isCompanionObject()) result.add("companion")
+                if (element.isFunctionalInterface()) result.add("fun")
+                if (element.isClass()) result.add("class")
+                if (element.isDataClass()) result.add("data")
+                if (element.isValueClass()) result.add("value")
+                if (element.isExpect()) result.add("expect")
                 if (element.isInterface()) result.add("interface")
                 if (element.isStatic()) result.add("static")
+                if (element.isAnnotationClass()) result.add("annotation")
                 return result
             }
 
@@ -235,32 +255,54 @@
             )
 
             assertThat(getModifiers("OpenClass"))
-                .containsExactly("public")
+                .containsExactly("public", "class")
             assertThat(getModifiers("AbstractClass"))
-                .containsExactly("abstract", "public")
+                .containsExactly("abstract", "public", "class")
             assertThat(getModifiers("MyObject"))
                 .containsExactly("final", "public", "object")
             assertThat(getModifiers("MyInterface"))
                 .containsExactly("abstract", "interface", "public")
             assertThat(getModifiers("Final"))
-                .containsExactly("final", "public")
+                .containsExactly("final", "public", "class")
             assertThat(getModifiers("PrivateClass"))
                 .containsExactlyElementsIn(
                     if (invocation.isKsp) {
-                        listOf("private", "final")
+                        listOf("private", "final", "class")
                     } else {
                         // java does not support top level private classes.
-                        listOf("final")
+                        listOf("final", "class")
                     }
                 )
             assertThat(getModifiers("OuterKotlinClass.InnerKotlinClass"))
-                .containsExactly("final", "public")
+                .containsExactly("final", "public", "class")
             assertThat(getModifiers("OuterKotlinClass.NestedKotlinClass"))
-                .containsExactly("final", "public", "static")
+                .containsExactly("final", "public", "static", "class")
             assertThat(getModifiers("OuterJavaClass.InnerJavaClass"))
-                .containsExactly("public")
+                .containsExactly("public", "class")
             assertThat(getModifiers("OuterJavaClass.NestedJavaClass"))
-                .containsExactly("public", "static")
+                .containsExactly("public", "static", "class")
+            assertThat(getModifiers("JavaAnnotation"))
+                .containsExactly("abstract", "public", "annotation")
+            assertThat(getModifiers("KotlinAnnotation")).apply {
+                // KSP vs KAPT metadata have a difference in final vs abstract modifiers
+                // for annotation types.
+                if (invocation.isKsp) {
+                    containsExactly("final", "public", "annotation")
+                } else {
+                    containsExactly("abstract", "public", "annotation")
+                }
+            }
+            assertThat(getModifiers("DataClass"))
+                .containsExactly("public", "final", "class", "data")
+            assertThat(getModifiers("InlineClass"))
+                .containsExactly("public", "final", "class", "value")
+
+            if (!invocation.isKsp) {
+                // TODO: Enable for ksp too once it supports fun interfaces
+                //  https://github.com/google/ksp/issues/393
+                assertThat(getModifiers("FunInterface"))
+                    .containsExactly("public", "abstract", "interface", "fun")
+            }
         }
     }