Repository: cashapp/redwood (kotlin)

Issue:
Switch children properties back to using lambdas
Now that we're not using the declared schema for testing and are going to take another approach (#335), we should restore the schema to using lambda types to define children. For unscoped children this change has no value, but for scoped children we can change this:
```kotlin
@Children(1, MyScope::Class) children: () -> Unit,
```
into this:
```kotlin
@Children(1) children: MyScope.() -> Unit,
```
which is much more natural and reflective of how it works in generated output.

Pull request:
Closes #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 5db62a3bfd..9520f20320 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
@@ -77,10 +77,19 @@ public annotation class Property(val tag: Int)
  *   @Children(1) val children: () -> Unit,
  * )
  * ```
+ *
+ * A class may be used as the lambda receiver to denote a scope. The receiver class will be
+ * propagated to the generated code and must be included in the [schema][Schema].
+ *
+ * ```
+ * @Widget(1)
+ * data class Box(
+ *   @Children(1) val children: BoxScope.() -> Unit,
+ * )
+ * ```
  */
 public annotation class Children(
   val tag: Int,
-  val scope: KClass<*> = Unit::class,
 )
 
 /**
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 fab60c0415..cb5d7fd041 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
@@ -130,11 +130,21 @@ private fun parseWidget(memberType: KClass<*>, annotation: WidgetAnnotation): Wi
         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.
+        var scope: KClass<*>? = null
+        var arguments = it.type.arguments.dropLast(1) // Drop return type.
+        if (it.type.annotations.any(ExtensionFunctionType::class::isInstance)) {
+          val receiverType = it.type.arguments.first().type
+          val receiverClassifier = receiverType?.classifier
+          require(receiverClassifier is KClass<*> && receiverType.arguments.isEmpty()) {
+            "@Children ${memberType.qualifiedName}#${it.name} lambda receiver can only be a class. Found: $receiverType"
+          }
+          scope = receiverClassifier
+          arguments = arguments.drop(1)
+        }
         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 })
+        Widget.Children(children.tag, it.name!!, scope)
       } else {
         throw IllegalArgumentException("Unannotated parameter \"${it.name}\" on ${memberType.qualifiedName}")
       }


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 a0536f18fd..4dfae81ba9 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,7 +33,7 @@ class ComposeGenerationTest {
 
   @Widget(1)
   data class Row(
-    @Children(1, RowScope::class) val scoped: () -> Unit,
+    @Children(1) val scoped: RowScope.() -> Unit,
     @Children(2) val unscoped: () -> Unit,
   )
 
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 f5352009cb..01819310a3 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
@@ -307,6 +307,69 @@ class SchemaParserTest {
     )
   }
 
+  @Schema(
+    [
+      ScopedChildrenArgumentsInvalidWidget::class,
+    ],
+  )
+  interface ScopedChildrenArgumentsInvalidSchema
+
+  @Widget(1)
+  data class ScopedChildrenArgumentsInvalidWidget(
+    @Children(1) val children: String.(Int) -> Unit,
+  )
+
+  @Test fun scopedChildrenArgumentsInvalid() {
+    assertThrows<IllegalArgumentException> {
+      parseSchema(ScopedChildrenArgumentsInvalidSchema::class)
+    }.hasMessageThat().isEqualTo(
+      "@Children app.cash.redwood.schema.parser.SchemaParserTest.ScopedChildrenArgumentsInvalidWidget#children lambda type must not have any arguments. " +
+        "Found: [kotlin.Int]",
+    )
+  }
+
+  @Schema(
+    [
+      ScopedChildrenInvalidWidget::class,
+    ],
+  )
+  interface ScopedChildrenInvalidSchema
+
+  @Widget(1)
+  data class ScopedChildrenInvalidWidget(
+    @Children(1) val children: List<Int>.() -> Unit,
+  )
+
+  @Test fun scopedChildrenInvalid() {
+    assertThrows<IllegalArgumentException> {
+      parseSchema(ScopedChildrenInvalidSchema::class)
+    }.hasMessageThat().isEqualTo(
+      "@Children app.cash.redwood.schema.parser.SchemaParserTest.ScopedChildrenInvalidWidget#children lambda receiver can only be a class. " +
+        "Found: kotlin.collections.List<kotlin.Int>",
+    )
+  }
+
+  @Schema(
+    [
+      ScopedChildrenTypeParameterInvalidWidget::class,
+    ],
+  )
+  interface ScopedChildrenTypeParameterInvalidSchema
+
+  @Widget(1)
+  data class ScopedChildrenTypeParameterInvalidWidget<T>(
+    @Children(1) val children: T.() -> Unit,
+  )
+
+  @Test fun scopedChildrenTypeParameterInvalid() {
+    assertThrows<IllegalArgumentException> {
+      parseSchema(ScopedChildrenTypeParameterInvalidSchema::class)
+    }.hasMessageThat().isEqualTo(
+      "@Children app.cash.redwood.schema.parser.SchemaParserTest.ScopedChildrenTypeParameterInvalidWidget#children lambda receiver can only be a class. " +
+        "Found: T",
+    )
+  }
+
   @Schema(
     [
       EventTypeWidget::class,
diff --git a/test-schema/src/main/kotlin/example/redwood/schema.kt b/test-schema/src/main/kotlin/example/redwood/schema.kt
index d1e69f77de..9711a82abe 100644
--- a/test-schema/src/main/kotlin/example/redwood/schema.kt
+++ b/test-schema/src/main/kotlin/example/redwood/schema.kt
@@ -49,7 +49,7 @@ public object RowScope
 
 @Widget(2)
 public data class ScopedRow(
-  @Children(1, RowScope::class) val children: () -> Unit,
+  @Children(1) val children: RowScope.() -> Unit,
 )
 
 @Widget(3)