[go: nahoru, domu]

[GH] [Room][XProcessing] Support for KSClassDeclaration association in filer

## Proposed Changes
Update XProcessing to use new KSP API `CodeGenerator.associateWithClasses` when writing files with originating elements so that classpath dependencies are accurately tracked.

## Testing

Test: Added tests to OriginatingElementsTest and KspFilerTest

## Issues Fixed

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

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

Resolves #285
Github-Pr-Head-Sha: 2a8c6f32e09f9861af9a178cf0ec4dae69ec1bf3
GitOrigin-RevId: 93d5c27594d8cea09fcd2c983f3fafbf3eb73e2e
Change-Id: I53950d737253ceec94d00da3f4d6989aba86da4b
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XElement.kt
index 60e92ca..a644cdd 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XElement.kt
@@ -20,7 +20,7 @@
 import androidx.room.compiler.processing.ksp.KSFileAsOriginatingElement
 import androidx.room.compiler.processing.ksp.KspElement
 import androidx.room.compiler.processing.ksp.KspMemberContainer
-import androidx.room.compiler.processing.ksp.containingFileAsOriginatingElement
+import androidx.room.compiler.processing.ksp.wrapAsOriginatingElement
 import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticPropertyMethodElement
 import javax.lang.model.element.Element
 import kotlin.contracts.contract
@@ -129,13 +129,13 @@
     return when (this) {
         is JavacElement -> element
         is KspElement -> {
-            declaration.containingFileAsOriginatingElement()
+            declaration.wrapAsOriginatingElement()
         }
         is KspSyntheticPropertyMethodElement -> {
-            field.declaration.containingFileAsOriginatingElement()
+            field.declaration.wrapAsOriginatingElement()
         }
         is KspMemberContainer -> {
-            declaration?.containingFileAsOriginatingElement()
+            declaration?.wrapAsOriginatingElement()
         }
         else -> error("Originating element is not implemented for ${this.javaClass}")
     }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAnnotatedExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAnnotatedExt.kt
index f10956c..20f735b 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAnnotatedExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAnnotatedExt.kt
@@ -17,6 +17,7 @@
 package androidx.room.compiler.processing.ksp
 
 import com.google.devtools.ksp.symbol.KSAnnotated
+import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.google.devtools.ksp.symbol.KSDeclaration
 
 private fun KSAnnotated.hasAnnotationWithQName(qName: String) = annotations.any {
@@ -33,11 +34,15 @@
 internal fun KSAnnotated.hasJvmDefaultAnnotation() = hasAnnotationWithQName("kotlin.jvm.JvmDefault")
 
 /**
- * Return a reference to the containing file that implements the
+ * Return a reference to the containing file or class declaration via a wrapper that implements the
  * [javax.lang.model.element.Element] API so that we can report it to JavaPoet.
  */
-internal fun KSAnnotated.containingFileAsOriginatingElement(): KSFileAsOriginatingElement? {
-    return (this as? KSDeclaration)?.containingFile?.let {
+internal fun KSAnnotated.wrapAsOriginatingElement(): OriginatingElementWrapper? {
+    val ksDeclaration = this as? KSDeclaration ?: return null
+
+    return ksDeclaration.containingFile?.let {
         KSFileAsOriginatingElement(it)
+    } ?: (ksDeclaration as? KSClassDeclaration)?.let {
+        KSClassDeclarationAsOriginatingElement(it)
     }
 }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt
index e2a0758..373d9607 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt
@@ -21,6 +21,7 @@
 import androidx.room.compiler.processing.util.ISSUE_TRACKER_LINK
 import com.google.devtools.ksp.processing.CodeGenerator
 import com.google.devtools.ksp.processing.Dependencies
+import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.google.devtools.ksp.symbol.KSFile
 import com.squareup.javapoet.JavaFile
 import com.squareup.kotlinpoet.FileSpec
@@ -34,11 +35,11 @@
     private val messager: XMessager,
 ) : XFiler {
     override fun write(javaFile: JavaFile, mode: XFiler.Mode) {
-        val originatingFiles = javaFile.typeSpec.originatingElements
-            .map(::originatingFileFor)
+        val originatingElements = javaFile.typeSpec.originatingElements
+            .toOriginatingElements()
 
         createNewFile(
-            originatingFiles = originatingFiles,
+            originatingElements = originatingElements,
             packageName = javaFile.packageName,
             fileName = javaFile.typeSpec.name,
             extensionName = "java",
@@ -51,13 +52,13 @@
     }
 
     override fun write(fileSpec: FileSpec, mode: XFiler.Mode) {
-        val originatingFiles = fileSpec.members
+        val originatingElements = fileSpec.members
             .filterIsInstance<OriginatingElementsHolder>()
             .flatMap { it.originatingElements }
-            .map(::originatingFileFor)
+            .toOriginatingElements()
 
         createNewFile(
-            originatingFiles = originatingFiles,
+            originatingElements = originatingElements,
             packageName = fileSpec.packageName,
             fileName = fileSpec.name,
             extensionName = "kt",
@@ -69,21 +70,14 @@
         }
     }
 
-    private fun originatingFileFor(element: Element): KSFile {
-        check(element is KSFileAsOriginatingElement) {
-            "Unexpected element type in originating elements. $element"
-        }
-        return element.ksFile
-    }
-
     private fun createNewFile(
-        originatingFiles: List<KSFile>,
+        originatingElements: OriginatingElements,
         packageName: String,
         fileName: String,
         extensionName: String,
         aggregating: Boolean
     ): OutputStream {
-        val dependencies = if (originatingFiles.isEmpty()) {
+        val dependencies = if (originatingElements.isEmpty()) {
             messager.printMessage(
                 Diagnostic.Kind.WARNING,
                 """
@@ -96,7 +90,16 @@
         } else {
             Dependencies(
                 aggregating = aggregating,
-                sources = originatingFiles.distinct().toTypedArray()
+                sources = originatingElements.files.distinct().toTypedArray()
+            )
+        }
+
+        if (originatingElements.classes.isNotEmpty()) {
+            delegate.associateWithClasses(
+                classes = originatingElements.classes,
+                packageName = packageName,
+                fileName = fileName,
+                extensionName = extensionName
             )
         }
 
@@ -107,4 +110,26 @@
             extensionName = extensionName
         )
     }
+
+    private data class OriginatingElements(
+        val files: List<KSFile>,
+        val classes: List<KSClassDeclaration>,
+    ) {
+        fun isEmpty(): Boolean = files.isEmpty() && classes.isEmpty()
+    }
+
+    private fun List<Element>.toOriginatingElements(): OriginatingElements {
+        val files = mutableListOf<KSFile>()
+        val classes = mutableListOf<KSClassDeclaration>()
+
+        forEach { element ->
+            when (element) {
+                is KSFileAsOriginatingElement -> files.add(element.ksFile)
+                is KSClassDeclarationAsOriginatingElement -> classes.add(element.ksClassDeclaration)
+                else -> error("Unexpected element type in originating elements. $element")
+            }
+        }
+
+        return OriginatingElements(files, classes)
+    }
 }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSFileAsOriginatingElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/OriginatingElementWrappers.kt
similarity index 72%
rename from room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSFileAsOriginatingElement.kt
rename to room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/OriginatingElementWrappers.kt
index ec05ba1..30aaa85 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSFileAsOriginatingElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/OriginatingElementWrappers.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.compiler.processing.ksp
 
+import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.google.devtools.ksp.symbol.KSFile
 import javax.lang.model.element.AnnotationMirror
 import javax.lang.model.element.Element
@@ -26,12 +27,27 @@
 import javax.lang.model.type.TypeMirror
 
 /**
- * When generating java code, JavaPoet only provides an API that receives Element.
- * This wrapper class helps us wrap a KSFile as an originating element and KspFiler unwraps it to
- * get the actual KSFile out of it.
+ * Implementation of [OriginatingElementWrapper] to wrap a [KSFile] for the case of a dependency
+ * within the sources being processed.
  */
 internal data class KSFileAsOriginatingElement(
     val ksFile: KSFile
+) : OriginatingElementWrapper(ksFile.fileName)
+
+/**
+ * Implementation of [OriginatingElementWrapper] to wrap a [KSClassDeclaration] for the case of
+ * a dependency in the classpath, in which case a [KSFile] is not available.
+ */
+internal data class KSClassDeclarationAsOriginatingElement(
+    val ksClassDeclaration: KSClassDeclaration
+) : OriginatingElementWrapper(ksClassDeclaration.simpleName.asString())
+
+/**
+ * When generating java code, JavaPoet only provides an API that receives Element.
+ * This wrapper class helps us wrap KSP constructs which KspFiler can unwrap later.
+ */
+internal sealed class OriginatingElementWrapper(
+    val elementSimpleName: String
 ) : Element {
     override fun getAnnotationMirrors(): List<AnnotationMirror> {
         return emptyList()
@@ -48,7 +64,7 @@
 
     override fun asType(): TypeMirror {
         throw UnsupportedOperationException(
-            "KSFileAsOriginatingElement cannot be converted to a type"
+            "${this::class.simpleName} cannot be converted to a type"
         )
     }
 
@@ -61,7 +77,7 @@
     }
 
     override fun getSimpleName(): Name {
-        return NameImpl(ksFile.fileName)
+        return NameImpl(elementSimpleName)
     }
 
     override fun getEnclosingElement(): Element? {
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/OriginatingElementsTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/OriginatingElementsTest.kt
index e32fb9a..1c18366 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/OriginatingElementsTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/OriginatingElementsTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.compiler.processing
 
+import androidx.room.compiler.processing.ksp.KSClassDeclarationAsOriginatingElement
 import androidx.room.compiler.processing.ksp.KSFileAsOriginatingElement
 import androidx.room.compiler.processing.ksp.KspTypeElement
 import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticPropertyMethodElement
@@ -72,6 +73,34 @@
     }
 
     @Test
+    fun classPathTypeIsConvertedToOriginatingElement() {
+        runProcessorTest {
+            val element = it.processingEnv
+                .requireTypeElement("com.google.devtools.ksp.processing.SymbolProcessor")
+
+            val originatingElement = element.originatingElementForPoet()
+            assertThat(originatingElement).isNotNull()
+
+            if (it.isKsp) {
+                assertThat(originatingElement)
+                    .isInstanceOf(KSClassDeclarationAsOriginatingElement::class.java)
+
+                val ksClassDeclaration =
+                    (originatingElement as KSClassDeclarationAsOriginatingElement)
+                        .ksClassDeclaration
+                assertThat(ksClassDeclaration)
+                    .isEqualTo(
+                        (element as KspTypeElement).declaration
+                    )
+            } else {
+                assertThat(originatingElement).isInstanceOf(TypeElement::class.java)
+                assertThat((originatingElement as TypeElement).qualifiedName.toString())
+                    .isEqualTo("com.google.devtools.ksp.processing.SymbolProcessor")
+            }
+        }
+    }
+
+    @Test
     fun syntheticPropertyElementConvertedToOriginatingElement() {
         runProcessorTest(
             sources = listOf(
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
index 252acfc..e3cbe2f 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
@@ -93,6 +93,56 @@
         }
     }
 
+    @Test
+    fun originatingClassAddedForClassPathAndFileType() {
+        runKspTest(sources = listOf(simpleKotlinClass)) { invocation ->
+            val sourceElement = invocation.processingEnv.requireTypeElement("foo.bar.Baz")
+            val classPathElement = invocation.processingEnv
+                .requireTypeElement("com.google.devtools.ksp.processing.SymbolProcessor")
+
+            val fileWithType = FileSpec.builder("foo", "Bar.kt").apply {
+                addType(
+                    TypeSpec.classBuilder("Bar").apply {
+                        addOriginatingElement(sourceElement)
+                        addOriginatingElement(classPathElement)
+                    }.build()
+                )
+            }.build()
+
+            val codeGenerator = DependencyTrackingCodeGenerator()
+            KspFiler(codeGenerator, TestMessager()).write(fileWithType)
+            codeGenerator.fileDependencies[fileWithType.name]
+                .containsExactlySimpleKotlinClass()
+            val (file, classDeclarations) = codeGenerator.classDependencies.entries.single()
+            assertThat(file).isEqualTo("Bar.kt")
+            assertThat(classDeclarations.single())
+                .isEqualTo((classPathElement as KspTypeElement).declaration)
+        }
+    }
+
+    @Test
+    fun originatingClassAddedForClassPathType() {
+        runKspTest(sources = listOf()) { invocation ->
+            val classPathElement = invocation.processingEnv
+                .requireTypeElement("com.google.devtools.ksp.processing.SymbolProcessor")
+
+            val fileWithType = FileSpec.builder("foo", "Bar.kt").apply {
+                addType(
+                    TypeSpec.classBuilder("Bar").apply {
+                        addOriginatingElement(classPathElement)
+                    }.build()
+                )
+            }.build()
+
+            val codeGenerator = DependencyTrackingCodeGenerator()
+            KspFiler(codeGenerator, TestMessager()).write(fileWithType)
+            val (file, classDeclarations) = codeGenerator.classDependencies.entries.single()
+            assertThat(file).isEqualTo("Bar.kt")
+            assertThat(classDeclarations.single())
+                .isEqualTo((classPathElement as KspTypeElement).declaration)
+        }
+    }
+
     private fun Dependencies?.containsExactlySimpleKotlinClass() {
         assertThat(this).isNotNull()
         val originatingFiles = this!!.originatingFiles.map { it.fileName }
@@ -122,6 +172,7 @@
     class DependencyTrackingCodeGenerator : CodeGenerator {
 
         val fileDependencies = mutableMapOf<String, Dependencies>()
+        val classDependencies = mutableMapOf<String, MutableSet<KSClassDeclaration>>()
 
         override val generatedFile: Collection<File>
             get() = emptyList()
@@ -141,7 +192,7 @@
             fileName: String,
             extensionName: String
         ) {
-            // no-op required override.
+            classDependencies.getOrPut(fileName) { mutableSetOf() }.addAll(classes)
         }
 
         override fun createNewFile(