Repository: cashapp/redwood
Pull request #348 (2022-09-07 17:56:00)

Switch lambda schema type back to lambda
This more closely resembles the Compose code, and will allow the use of a receiver in the future to denote scope. Our current parsing mechanism does not provide access to the receiver in any meaningful way that we can differentiate from a normal lambda argument so its support is deferred.

Refs #336


Patch:
diff --git a/redwood-schema-annotations/src/commonMain/kotlin/app/cash/redwood/schema/annotations.kt b/redwood-schema-annotations/src/commonMain/kotlin/app/cash/redwood/schema/annotations.kt
index edbf373ce..5db62a3bf 100644
--- a/redwood-schema-annotations/src/commonMain/kotlin/app/cash/redwood/schema/annotations.kt
+++ b/redwood-schema-annotations/src/commonMain/kotlin/app/cash/redwood/schema/annotations.kt
@@ -46,7 +46,7 @@ public annotation class Schema(val members: Array<KClass<*>>)
  * @Widget(1)
  * data class Box(
  *   @Property(1) val orientation: Orientation,
- *   @Children val children: List<Any>,
+ *   @Children val children: () -> Unit,
  * )
  * ```
  */
@@ -69,12 +69,12 @@ public annotation class Property(val tag: Int)
 /**
  * Annotates a [Widget] property as representing child widgets which are contained within the
  * enclosing widget. Children in a [Widget] class must have a unique [tag] among all
- * [@Children][Children] annotations in the class. The type of the property must be `List<Any>`.
+ * [@Children][Children] annotations in the class. The type of the property must be `() -> Unit`.
  *
  * ```
  * @Widget(1)
  * data class Box(
- *   @Children(1) val children: List<Any>,
+ *   @Children(1) val children: () -> Unit,
  * )
  * ```
  */
diff --git a/redwood-schema/src/main/kotlin/app/cash/redwood/schema/parser/schemaParser.kt b/redwood-schema/src/main/kotlin/app/cash/redwood/schema/parser/schemaParser.kt
index 9faa5e216..fab60c041 100644
--- a/redwood-schema/src/main/kotlin/app/cash/redwood/schema/parser/schemaParser.kt
+++ b/redwood-schema/src/main/kotlin/app/cash/redwood/schema/parser/schemaParser.kt
@@ -22,17 +22,13 @@ import app.cash.redwood.schema.Property as PropertyAnnotation
 import app.cash.redwood.schema.Schema as SchemaAnnotation
 import app.cash.redwood.schema.Widget as WidgetAnnotation
 import kotlin.reflect.KClass
-import kotlin.reflect.KTypeProjection.Companion.invariant
-import kotlin.reflect.full.createType
 import kotlin.reflect.full.findAnnotation
 import kotlin.reflect.full.isSubtypeOf
 import kotlin.reflect.full.primaryConstructor
 import kotlin.reflect.full.starProjectedType
 import kotlin.reflect.full.withNullability
 
-private val childrenType = List::class.createType(
-  arguments = listOf(invariant(Any::class.createType())),
-)
+private val childrenType = Function::class.starProjectedType
 private val eventType = Function::class.starProjectedType
 private val optionalEventType = eventType.withNullability(true)
 
@@ -131,8 +127,12 @@ private fun parseWidget(memberType: KClass<*>, annotation: WidgetAnnotation): Wi
           Widget.Property(property.tag, it.name!!, it.type, defaultExpression)
         }
       } else if (children != null) {
-        require(it.type == childrenType) {
-          "@Children ${memberType.qualifiedName}#${it.name} must be of type 'List<Any>'"
+        require(it.type.isSubtypeOf(childrenType)) {
+          "@Children ${memberType.qualifiedName}#${it.name} must be of type '() -> Unit'"
+        }
+        val arguments = it.type.arguments.dropLast(1) // Drop return type.
+        require(arguments.isEmpty()) {
+          "@Children ${memberType.qualifiedName}#${it.name} lambda type must not have any arguments. Found: $arguments"
         }
         Widget.Children(children.tag, it.name!!, children.scope.takeIf { it != Unit::class })
       } else {
diff --git a/samples/counter/sunspot/src/main/kotlin/example/sunspot/sunspot.kt b/samples/counter/sunspot/src/main/kotlin/example/sunspot/sunspot.kt
index 576ae770a..3f6370be3 100644
--- a/samples/counter/sunspot/src/main/kotlin/example/sunspot/sunspot.kt
+++ b/samples/counter/sunspot/src/main/kotlin/example/sunspot/sunspot.kt
@@ -32,7 +32,7 @@ interface Sunspot
 
 @Widget(1)
 data class SunspotBox(
-  @Children(1) val children: List<Any>,
+  @Children(1) val children: () -> Unit,
 )
 
 @Widget(2)
diff --git a/samples/emoji-search/schema/src/main/kotlin/example/schema/schema.kt b/samples/emoji-search/schema/src/main/kotlin/example/schema/schema.kt
index 2120be3c7..3eeb9d2c4 100644
--- a/samples/emoji-search/schema/src/main/kotlin/example/schema/schema.kt
+++ b/samples/emoji-search/schema/src/main/kotlin/example/schema/schema.kt
@@ -32,12 +32,12 @@ interface EmojiSearch
 
 @Widget(1)
 data class Column(
-  @Children(1) val children: List<Any>,
+  @Children(1) val children: () -> Unit,
 )
 
 @Widget(2)
 data class ScrollableColumn(
-  @Children(1) val children: List<Any>,
+  @Children(1) val children: () -> Unit,
 )
 
 @Widget(3)
diff --git a/samples/todo-list/schema/src/main/kotlin/example/schema/Todo.kt b/samples/todo-list/schema/src/main/kotlin/example/schema/Todo.kt
index c2f3bc13a..5055d3dff 100644
--- a/samples/todo-list/schema/src/main/kotlin/example/schema/Todo.kt
+++ b/samples/todo-list/schema/src/main/kotlin/example/schema/Todo.kt
@@ -37,7 +37,7 @@ data class Toolbar(
 
 @Widget(2)
 data class ScrollableColumn(
-  @Children(1) val children: List<Any>,
+  @Children(1) val children: () -> Unit,
 )
 
 @Widget(3)
@@ -48,5 +48,5 @@ data class Item(
 
 @Widget(4)
 data class Column(
-  @Children(1) val children: List<Any>,
+  @Children(1) val children: () -> Unit,
 )


Test patch:
diff --git a/redwood-cli/src/test/kotlin/app/cash/redwood/generator/ComposeGenerationTest.kt b/redwood-cli/src/test/kotlin/app/cash/redwood/generator/ComposeGenerationTest.kt
index 3c50acd7a..a0536f18f 100644
--- a/redwood-cli/src/test/kotlin/app/cash/redwood/generator/ComposeGenerationTest.kt
+++ b/redwood-cli/src/test/kotlin/app/cash/redwood/generator/ComposeGenerationTest.kt
@@ -33,8 +33,8 @@ class ComposeGenerationTest {
 
   @Widget(1)
   data class Row(
-    @Children(1, RowScope::class) val scoped: List<Any>,
-    @Children(2) val unscoped: List<Any>,
+    @Children(1, RowScope::class) val scoped: () -> Unit,
+    @Children(2) val unscoped: () -> Unit,
   )
 
   @Test fun functionTargetMarker() {
diff --git a/redwood-gradle-plugin/src/test/fixture/compose-ui/schema/src/main/kotlin/example/counter/schema.kt b/redwood-gradle-plugin/src/test/fixture/compose-ui/schema/src/main/kotlin/example/counter/schema.kt
index 86255846e..67472cd8c 100644
--- a/redwood-gradle-plugin/src/test/fixture/compose-ui/schema/src/main/kotlin/example/counter/schema.kt
+++ b/redwood-gradle-plugin/src/test/fixture/compose-ui/schema/src/main/kotlin/example/counter/schema.kt
@@ -32,7 +32,7 @@ interface Counter
 
 @Widget(1)
 data class CounterBox(
-  @Children(1) val children: List<Any>,
+  @Children(1) val children: () -> Unit,
 )
 
 @Widget(2)
diff --git a/redwood-gradle-plugin/src/test/fixture/protocol-no-layout-modifiers/src/main/kotlin/example/counter/schema.kt b/redwood-gradle-plugin/src/test/fixture/protocol-no-layout-modifiers/src/main/kotlin/example/counter/schema.kt
index a27a479c9..037932218 100644
--- a/redwood-gradle-plugin/src/test/fixture/protocol-no-layout-modifiers/src/main/kotlin/example/counter/schema.kt
+++ b/redwood-gradle-plugin/src/test/fixture/protocol-no-layout-modifiers/src/main/kotlin/example/counter/schema.kt
@@ -30,5 +30,5 @@ interface Counter
 
 @Widget(1)
 data class CounterBox(
-  @Children(1) val children: List<Any>,
+  @Children(1) val children: () -> Unit,
 )
diff --git a/redwood-gradle-plugin/src/test/fixture/schema-project-accessor/schema/src/main/kotlin/example/counter/schema.kt b/redwood-gradle-plugin/src/test/fixture/schema-project-accessor/schema/src/main/kotlin/example/counter/schema.kt
index 86255846e..67472cd8c 100644
--- a/redwood-gradle-plugin/src/test/fixture/schema-project-accessor/schema/src/main/kotlin/example/counter/schema.kt
+++ b/redwood-gradle-plugin/src/test/fixture/schema-project-accessor/schema/src/main/kotlin/example/counter/schema.kt
@@ -32,7 +32,7 @@ interface Counter
 
 @Widget(1)
 data class CounterBox(
-  @Children(1) val children: List<Any>,
+  @Children(1) val children: () -> Unit,
 )
 
 @Widget(2)
diff --git a/redwood-gradle-plugin/src/test/fixture/schema-project-reference/schema/src/main/kotlin/example/counter/schema.kt b/redwood-gradle-plugin/src/test/fixture/schema-project-reference/schema/src/main/kotlin/example/counter/schema.kt
index 86255846e..67472cd8c 100644
--- a/redwood-gradle-plugin/src/test/fixture/schema-project-reference/schema/src/main/kotlin/example/counter/schema.kt
+++ b/redwood-gradle-plugin/src/test/fixture/schema-project-reference/schema/src/main/kotlin/example/counter/schema.kt
@@ -32,7 +32,7 @@ interface Counter
 
 @Widget(1)
 data class CounterBox(
-  @Children(1) val children: List<Any>,
+  @Children(1) val children: () -> Unit,
 )
 
 @Widget(2)
diff --git a/redwood-schema/src/test/kotlin/app/cash/redwood/schema/parser/SchemaParserTest.kt b/redwood-schema/src/test/kotlin/app/cash/redwood/schema/parser/SchemaParserTest.kt
index 9a856763b..f5352009c 100644
--- a/redwood-schema/src/test/kotlin/app/cash/redwood/schema/parser/SchemaParserTest.kt
+++ b/redwood-schema/src/test/kotlin/app/cash/redwood/schema/parser/SchemaParserTest.kt
@@ -207,9 +207,9 @@ class SchemaParserTest {
 
   @Widget(1)
   data class DuplicateChildrenTagWidget(
-    @Children(1) val childrenA: List<Any>,
+    @Children(1) val childrenA: () -> Unit,
     @Property(1) val name: String,
-    @Children(1) val childrenB: List<Any>,
+    @Children(1) val childrenB: () -> Unit,
   )
 
   @Test fun duplicateChildrenTagThrows() {
@@ -234,7 +234,7 @@ class SchemaParserTest {
   @Widget(1)
   data class UnannotatedPrimaryParameterWidget(
     @Property(1) val name: String,
-    @Children(1) val children: List<Any>,
+    @Children(1) val children: () -> Unit,
     val unannotated: String,
   )
 
@@ -275,14 +275,35 @@ class SchemaParserTest {
 
   @Widget(1)
   data class InvalidChildrenTypeWidget(
-    @Children(1) val children: List<String>,
+    @Children(1) val children: String,
   )
 
   @Test fun invalidChildrenTypeThrows() {
     assertThrows<IllegalArgumentException> {
       parseSchema(InvalidChildrenTypeSchema::class)
     }.hasMessageThat().isEqualTo(
-      "@Children app.cash.redwood.schema.parser.SchemaParserTest.InvalidChildrenTypeWidget#children must be of type 'List<Any>'",
+      "@Children app.cash.redwood.schema.parser.SchemaParserTest.InvalidChildrenTypeWidget#children must be of type '() -> Unit'",
+    )
+  }
+
+  @Schema(
+    [
+      ChildrenArgumentsInvalidWidget::class,
+    ],
+  )
+  interface ChildrenArgumentsInvalidSchema
+
+  @Widget(1)
+  data class ChildrenArgumentsInvalidWidget(
+    @Children(1) val children: (String) -> Unit,
+  )
+
+  @Test fun childrenArgumentsInvalid() {
+    assertThrows<IllegalArgumentException> {
+      parseSchema(ChildrenArgumentsInvalidSchema::class)
+    }.hasMessageThat().isEqualTo(
+      "@Children app.cash.redwood.schema.parser.SchemaParserTest.ChildrenArgumentsInvalidWidget#children lambda type must not have any arguments. " +
+        "Found: [kotlin.String]",
     )
   }
 
diff --git a/test-schema/src/main/kotlin/example/redwood/schema.kt b/test-schema/src/main/kotlin/example/redwood/schema.kt
index 1598c067b..d1e69f77d 100644
--- a/test-schema/src/main/kotlin/example/redwood/schema.kt
+++ b/test-schema/src/main/kotlin/example/redwood/schema.kt
@@ -42,14 +42,14 @@ public interface ExampleSchema
 
 @Widget(1)
 public data class Row(
-  @Children(1) val children: List<Any>,
+  @Children(1) val children: () -> Unit,
 )
 
 public object RowScope
 
 @Widget(2)
 public data class ScopedRow(
-  @Children(1, RowScope::class) val children: List<Any>,
+  @Children(1, RowScope::class) val children: () -> Unit,
 )
 
 @Widget(3)