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,
+                    )
                 )
         }
     }