Merge "Synthesize a no-arg constructor in KSP when primary has all default values" into androidx-main
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotation.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotation.kt
index 9eaf170..13ace16 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotation.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotation.kt
@@ -20,6 +20,7 @@
import androidx.room.compiler.processing.XAnnotationBox
import androidx.room.compiler.processing.XAnnotationValue
import androidx.room.compiler.processing.XType
+import com.google.devtools.ksp.getConstructors
import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.Origin
@@ -52,11 +53,19 @@
override val annotationValues: List<XAnnotationValue> by lazy {
// In KSP the annotation members may be represented by constructor parameters in kotlin
// source or by abstract methods in java source so we check both.
- val typesByName = if (typeElement.getConstructors().single().parameters.isNotEmpty()) {
- typeElement.getConstructors()
- .single()
- .parameters
- .associate { it.name to it.type }
+ val declarationConstructors = typeElement.let {
+ // We access constructor using declaration since for compatibility with KAPT,
+ // XTypeElement.getConstructors() will return an empty list for annotation classes.
+ check(it is KspTypeElement)
+ it.declaration.getConstructors().map {
+ KspConstructorElement(
+ env = env,
+ declaration = it
+ )
+ }
+ }
+ val typesByName = if (declarationConstructors.single().parameters.isNotEmpty()) {
+ declarationConstructors.single().parameters.associate { it.name to it.type }
} else {
typeElement.getDeclaredMethods()
.filter { it.isAbstract() }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspConstructorType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspConstructorType.kt
index 87e4bef..d86a3e9 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspConstructorType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspConstructorType.kt
@@ -16,10 +16,12 @@
package androidx.room.compiler.processing.ksp
+import androidx.room.compiler.processing.XConstructorElement
import androidx.room.compiler.processing.XConstructorType
-internal class KspConstructorType(
+internal class KspConstructorType<ConstructorElement>(
env: KspProcessingEnv,
- override val origin: KspConstructorElement,
+ override val origin: ConstructorElement,
containing: KspType?
) : KspExecutableType(env, origin, containing), XConstructorType
+ where ConstructorElement : KspExecutableElement, ConstructorElement : XConstructorElement
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 2e724a5..8d2d033c 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
@@ -33,6 +33,7 @@
import androidx.room.compiler.processing.collectFieldsIncludingPrivateSupers
import androidx.room.compiler.processing.filterMethodsByConfig
import androidx.room.compiler.processing.ksp.KspAnnotated.UseSiteFilter.Companion.NO_USE_SITE
+import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticNoArgConstructorElement
import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticPropertyMethodElement
import androidx.room.compiler.processing.tryBox
import androidx.room.compiler.processing.util.MemoizedSequence
@@ -262,6 +263,9 @@
}
override fun findPrimaryConstructor(): XConstructorElement? {
+ if (isAnnotationClass()) {
+ return null
+ }
return declaration.primaryConstructor?.let {
KspConstructorElement(
env = env,
@@ -323,12 +327,34 @@
}
override fun getConstructors(): List<XConstructorElement> {
- return declaration.getConstructors().map {
- KspConstructorElement(
- env = env,
- declaration = it
+ if (isAnnotationClass()) {
+ return emptyList()
+ }
+ return buildList {
+ addAll(
+ declaration.getConstructors().map {
+ KspConstructorElement(
+ env = env,
+ declaration = it
+ )
+ }
)
- }.toList()
+ // Too match KAPT if all params in the primary constructor have default values then
+ // synthesize a no-arg constructor if one is not already present.
+ val hasNoArgConstructor = declaration.getConstructors().any { it.parameters.isEmpty() }
+ if (!hasNoArgConstructor) {
+ declaration.primaryConstructor?.let {
+ if (it.parameters.all { it.hasDefault }) {
+ add(
+ KspSyntheticNoArgConstructorElement(
+ env = env,
+ declaration = it
+ )
+ )
+ }
+ }
+ }
+ }
}
override fun getSuperInterfaceElements(): List<XTypeElement> {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticNoArgConstructorElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticNoArgConstructorElement.kt
new file mode 100644
index 0000000..bcdca26
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticNoArgConstructorElement.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing.ksp.synthetic
+
+import androidx.room.compiler.processing.XConstructorElement
+import androidx.room.compiler.processing.XConstructorType
+import androidx.room.compiler.processing.XExecutableParameterElement
+import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.ksp.KspConstructorType
+import androidx.room.compiler.processing.ksp.KspExecutableElement
+import androidx.room.compiler.processing.ksp.KspProcessingEnv
+import androidx.room.compiler.processing.ksp.KspType
+import androidx.room.compiler.processing.ksp.KspTypeElement
+import androidx.room.compiler.processing.ksp.requireEnclosingMemberContainer
+import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+
+/**
+ * Represents the no-arg constructor of a type element for which all parameters in the primary
+ * constructor have default values.
+ */
+internal class KspSyntheticNoArgConstructorElement(
+ env: KspProcessingEnv,
+ declaration: KSFunctionDeclaration
+) : KspExecutableElement(env, declaration), XConstructorElement {
+
+ override val enclosingElement: KspTypeElement by lazy {
+ declaration.requireEnclosingMemberContainer(env) as? KspTypeElement
+ ?: error("Constructor parent must be a type element $this")
+ }
+
+ override val name: String
+ get() = "<init>"
+
+ override val parameters: List<XExecutableParameterElement>
+ get() = emptyList()
+
+ override val executableType: XConstructorType by lazy {
+ KspConstructorType(
+ env = env,
+ origin = this,
+ containing = this.enclosingElement.type
+ )
+ }
+
+ override fun asMemberOf(other: XType): XConstructorType {
+ check(other is KspType)
+ return KspConstructorType(
+ env = env,
+ origin = this,
+ containing = other
+ )
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
index d2822e3..c47fcb8 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
@@ -1438,13 +1438,14 @@
}
abstract class AbstractNoExplicit
abstract class AbstractExplicit(x:Int)
+ annotation class AnnotationClass
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
val subjects = listOf(
"MyInterface", "NoExplicitConstructor", "Base", "ExplicitConstructor",
"BaseWithSecondary", "Sub", "SubWith3Constructors",
- "AbstractNoExplicit", "AbstractExplicit"
+ "AbstractNoExplicit", "AbstractExplicit", "AnnotationClass"
)
val constructorCounts = subjects.map {
it to invocation.processingEnv.requireTypeElement(it).getConstructors().size
@@ -1459,7 +1460,8 @@
"Sub" to 1,
"SubWith3Constructors" to 3,
"AbstractNoExplicit" to 1,
- "AbstractExplicit" to 1
+ "AbstractExplicit" to 1,
+ "AnnotationClass" to 0
)
val primaryConstructorParameterNames = subjects.map {
@@ -1479,7 +1481,75 @@
"Sub" to listOf("x"),
"SubWith3Constructors" to emptyList<String>(),
"AbstractNoExplicit" to emptyList<String>(),
- "AbstractExplicit" to listOf("x")
+ "AbstractExplicit" to listOf("x"),
+ "AnnotationClass" to null
+ )
+ }
+ }
+
+ @Test
+ fun constructorsWithDefaultValues() {
+ val src = Source.kotlin(
+ "Subject.kt",
+ """
+ // These should have a no-arg constructor
+ class DefaultCtor
+ class DefaultArgsPrimary(val x: String = "")
+ class AlreadyHasPrimaryNoArgsCtor() {
+ constructor(y: Int = 1) : this()
+ }
+ class AlreadyHasSecondaryNoArgsCtor(val x: String) {
+ constructor() : this("")
+ }
+
+ // These can't have no arg constructor
+ class DefaultArgsSecondary(val x: String) {
+ constructor(y: Int = 1) : this("")
+ }
+ class CantHaveNoArgsCtor(val x: String = "", val y: Int)
+
+ // Shouldn't synthesize no-arg for annotation classes
+ annotation class AnnotationClass(val x: String = "")
+ """.trimIndent()
+ )
+ runTest(sources = listOf(src)) { invocation ->
+ val subjects = listOf(
+ "DefaultCtor", "DefaultArgsPrimary", "AlreadyHasPrimaryNoArgsCtor",
+ "AlreadyHasSecondaryNoArgsCtor", "DefaultArgsSecondary", "CantHaveNoArgsCtor",
+ "AnnotationClass"
+ )
+ val constructorCounts =
+ subjects.associateWith {
+ invocation.processingEnv.requireTypeElement(it).getConstructors().size
+ }
+ assertThat(constructorCounts)
+ .containsExactlyEntriesIn(
+ mapOf(
+ "DefaultCtor" to 1,
+ "DefaultArgsPrimary" to 2,
+ "AlreadyHasPrimaryNoArgsCtor" to 2,
+ "AlreadyHasSecondaryNoArgsCtor" to 2,
+ "DefaultArgsSecondary" to 2,
+ "CantHaveNoArgsCtor" to 1,
+ "AnnotationClass" to 0,
+ )
+ )
+
+ val hasNoArgConstructor = subjects.associateWith {
+ invocation.processingEnv.requireTypeElement(it)
+ .getConstructors().any { it.parameters.isEmpty() }
+ }
+ assertThat(hasNoArgConstructor)
+ .containsExactlyEntriesIn(
+ mapOf(
+ "DefaultCtor" to true,
+ "DefaultArgsPrimary" to true,
+ "AlreadyHasPrimaryNoArgsCtor" to true,
+ "AlreadyHasSecondaryNoArgsCtor" to true,
+ "DefaultArgsSecondary" to false,
+ "CantHaveNoArgsCtor" to false,
+ "AnnotationClass" to false,
+ )
)
}
}