[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(