Repository: cashapp/redwood
Pull request #814 (2023-02-09 04:21:45)

Do not embed dependencies as full schemas
Instead we only reference the `FqType`, and expose a new type to return a schema and its dependencies.

Next step is to read dependencies from JSON rather than reflection.

Refs #19


Patch:
diff --git a/redwood-cli/src/main/kotlin/app/cash/redwood/cli/GenerateCommand.kt b/redwood-cli/src/main/kotlin/app/cash/redwood/cli/GenerateCommand.kt
index 5c119ff33..d37a93f53 100644
--- a/redwood-cli/src/main/kotlin/app/cash/redwood/cli/GenerateCommand.kt
+++ b/redwood-cli/src/main/kotlin/app/cash/redwood/cli/GenerateCommand.kt
@@ -62,12 +62,12 @@ internal class GenerateCommand : CliktCommand(name = "generate") {
 
     when (val type = type) {
       is CodegenType -> {
-        val schema = parseSchema(schemaType)
-        schema.generate(type, out)
+        val schemaSet = parseSchema(schemaType)
+        schemaSet.generate(type, out)
       }
       is ProtocolCodegenType -> {
-        val schema = parseProtocolSchema(schemaType)
-        schema.generate(type, out)
+        val schemaSet = parseProtocolSchema(schemaType)
+        schemaSet.generate(type, out)
       }
       else -> throw AssertionError()
     }
diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/codegen.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/codegen.kt
index fa212bdd7..cb9d02aa0 100644
--- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/codegen.kt
+++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/codegen.kt
@@ -19,7 +19,7 @@ import app.cash.redwood.tooling.codegen.CodegenType.Compose
 import app.cash.redwood.tooling.codegen.CodegenType.LayoutModifiers
 import app.cash.redwood.tooling.codegen.CodegenType.Testing
 import app.cash.redwood.tooling.codegen.CodegenType.Widget
-import app.cash.redwood.tooling.schema.Schema
+import app.cash.redwood.tooling.schema.SchemaSet
 import java.nio.file.Path
 
 public enum class CodegenType {
@@ -29,35 +29,35 @@ public enum class CodegenType {
   Widget,
 }
 
-public fun Schema.generate(type: CodegenType, destination: Path) {
+public fun SchemaSet.generate(type: CodegenType, destination: Path) {
   when (type) {
     Compose -> {
-      generateLayoutModifierImpls(this)?.writeTo(destination)
-      for (scope in scopes) {
-        generateScope(this, scope).writeTo(destination)
+      generateLayoutModifierImpls(schema)?.writeTo(destination)
+      for (scope in schema.scopes) {
+        generateScope(schema, scope).writeTo(destination)
       }
-      for (widget in widgets) {
-        generateComposable(this, widget).writeTo(destination)
+      for (widget in schema.widgets) {
+        generateComposable(schema, widget).writeTo(destination)
       }
     }
     LayoutModifiers -> {
-      for (layoutModifier in layoutModifiers) {
-        generateLayoutModifierInterface(this, layoutModifier).writeTo(destination)
+      for (layoutModifier in schema.layoutModifiers) {
+        generateLayoutModifierInterface(schema, layoutModifier).writeTo(destination)
       }
     }
     Testing -> {
       generateTester(this).writeTo(destination)
-      generateMutableWidgetFactory(this).writeTo(destination)
-      for (widget in widgets) {
-        generateMutableWidget(this, widget).writeTo(destination)
-        generateWidgetValue(this, widget).writeTo(destination)
+      generateMutableWidgetFactory(schema).writeTo(destination)
+      for (widget in schema.widgets) {
+        generateMutableWidget(schema, widget).writeTo(destination)
+        generateWidgetValue(schema, widget).writeTo(destination)
       }
     }
     Widget -> {
       generateWidgetFactories(this).writeTo(destination)
-      generateWidgetFactory(this).writeTo(destination)
-      for (widget in widgets) {
-        generateWidget(this, widget).writeTo(destination)
+      generateWidgetFactory(schema).writeTo(destination)
+      for (widget in schema.widgets) {
+        generateWidget(schema, widget).writeTo(destination)
       }
     }
   }
diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/composeProtocolGeneration.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/composeProtocolGeneration.kt
index 7ad886a97..155485397 100644
--- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/composeProtocolGeneration.kt
+++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/composeProtocolGeneration.kt
@@ -16,6 +16,7 @@
 package app.cash.redwood.tooling.codegen
 
 import app.cash.redwood.tooling.schema.ProtocolSchema
+import app.cash.redwood.tooling.schema.ProtocolSchemaSet
 import app.cash.redwood.tooling.schema.ProtocolWidget
 import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolChildren
 import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolEvent
@@ -80,8 +81,9 @@ class ExampleProtocolBridge(
 }
 */
 internal fun generateProtocolBridge(
-  schema: ProtocolSchema,
+  schemaSet: ProtocolSchemaSet,
 ): FileSpec {
+  val schema = schemaSet.schema
   val type = schema.protocolBridgeType()
   val providerType = schema.getWidgetFactoryProviderType().parameterizedBy(NOTHING)
   return FileSpec.builder(type.packageName, type.simpleName)
@@ -149,7 +151,7 @@ internal fun generateProtocolBridge(
                 .addStatement("val root = bridge.widgetChildren(%T.Root, %T.Root)", Protocol.Id, Protocol.ChildrenTag)
                 .apply {
                   val arguments = buildList {
-                    for (dependency in schema.allSchemas) {
+                    for (dependency in schemaSet.all) {
                       add(CodeBlock.of("%N = %T(bridge, json, mismatchHandler)", dependency.type.flatName, dependency.protocolWidgetFactoryType(schema)))
                     }
                   }
@@ -666,8 +668,9 @@ internal fun generateProtocolLayoutModifierSerializers(
 }
 
 internal fun generateProtocolLayoutModifierSerialization(
-  schema: ProtocolSchema,
+  schemaSet: ProtocolSchemaSet,
 ): FileSpec {
+  val schema = schemaSet.schema
   val name = schema.layoutModifierToProtocol.simpleName
   return FileSpec.builder(schema.composePackage(), "layoutModifierSerialization")
     .addFunction(
@@ -693,7 +696,7 @@ internal fun generateProtocolLayoutModifierSerialization(
         .returns(Protocol.LayoutModifierElement)
         .beginControlFlow("return when (this)")
         .apply {
-          val layoutModifiers = schema.allLayoutModifiers()
+          val layoutModifiers = schemaSet.allLayoutModifiers()
           if (layoutModifiers.isEmpty()) {
             addAnnotation(
               AnnotationSpec.builder(Suppress::class)
diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/diffConsumingGeneration.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/diffConsumingGeneration.kt
index 5da4d463f..6d96f20fe 100644
--- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/diffConsumingGeneration.kt
+++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/diffConsumingGeneration.kt
@@ -16,6 +16,7 @@
 package app.cash.redwood.tooling.codegen
 
 import app.cash.redwood.tooling.schema.ProtocolSchema
+import app.cash.redwood.tooling.schema.ProtocolSchemaSet
 import app.cash.redwood.tooling.schema.ProtocolWidget
 import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolChildren
 import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolEvent
@@ -57,8 +58,9 @@ public class SunspotDiffConsumingNodeFactory<W : Any>(
 }
 */
 internal fun generateDiffConsumingNodeFactory(
-  schema: ProtocolSchema,
+  schemaSet: ProtocolSchemaSet,
 ): FileSpec {
+  val schema = schemaSet.schema
   val provider = schema.getWidgetFactoryProviderType().parameterizedBy(typeVariableW)
   val type = schema.diffConsumingNodeFactoryType()
   return FileSpec.builder(type.packageName, type.simpleName)
@@ -108,7 +110,7 @@ internal fun generateDiffConsumingNodeFactory(
             )
             .beginControlFlow("return when (tag.value)")
             .apply {
-              for (dependency in schema.allSchemas.sortedBy { it.widgets.firstOrNull()?.tag ?: 0 }) {
+              for (dependency in schemaSet.all.sortedBy { it.widgets.firstOrNull()?.tag ?: 0 }) {
                 for (widget in dependency.widgets.sortedBy { it.tag }) {
                   addStatement(
                     "%L -> %T(parentId, parentChildren, provider.%N.%N(), json, mismatchHandler)",
@@ -338,11 +340,11 @@ internal fun generateDiffConsumingWidget(
 }
 
 internal fun generateDiffConsumingLayoutModifierSerialization(
-  schema: ProtocolSchema,
+  schemaSet: ProtocolSchemaSet,
 ): FileSpec {
-  return FileSpec.builder(schema.widgetPackage(), "layoutModifierSerialization")
-    .addFunction(generateJsonArrayToLayoutModifier(schema))
-    .addFunction(generateJsonElementToLayoutModifier(schema))
+  return FileSpec.builder(schemaSet.schema.widgetPackage(), "layoutModifierSerialization")
+    .addFunction(generateJsonArrayToLayoutModifier(schemaSet.schema))
+    .addFunction(generateJsonElementToLayoutModifier(schemaSet))
     .build()
 }
 
@@ -417,7 +419,7 @@ private fun generateJsonArrayToLayoutModifier(schema: ProtocolSchema): FunSpec {
     .build()
 }
 
-private fun generateJsonElementToLayoutModifier(schema: ProtocolSchema): FunSpec {
+private fun generateJsonElementToLayoutModifier(schemaSet: ProtocolSchemaSet): FunSpec {
   return FunSpec.builder("toLayoutModifier")
     .addModifiers(PRIVATE)
     .receiver(Protocol.LayoutModifierElement)
@@ -426,7 +428,7 @@ private fun generateJsonElementToLayoutModifier(schema: ProtocolSchema): FunSpec
     .returns(Redwood.LayoutModifier)
     .beginControlFlow("val serializer = when (tag.value)")
     .apply {
-      val layoutModifiers = schema.allLayoutModifiers()
+      val layoutModifiers = schemaSet.allLayoutModifiers()
       if (layoutModifiers.isEmpty()) {
         addAnnotation(
           AnnotationSpec.builder(Suppress::class)
@@ -434,8 +436,9 @@ private fun generateJsonElementToLayoutModifier(schema: ProtocolSchema): FunSpec
             .build(),
         )
       } else {
+        val host = schemaSet.schema
         for ((localSchema, layoutModifier) in layoutModifiers) {
-          val typeName = ClassName(localSchema.widgetPackage(schema), layoutModifier.type.flatName + "Impl")
+          val typeName = ClassName(localSchema.widgetPackage(host), layoutModifier.type.flatName + "Impl")
           if (layoutModifier.properties.isEmpty()) {
             addStatement("%L -> return %T", layoutModifier.tag, typeName)
           } else {
diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolCodegen.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolCodegen.kt
index 9d4d7bdc4..c5137dfc3 100644
--- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolCodegen.kt
+++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolCodegen.kt
@@ -18,7 +18,7 @@ package app.cash.redwood.tooling.codegen
 import app.cash.redwood.tooling.codegen.ProtocolCodegenType.Compose
 import app.cash.redwood.tooling.codegen.ProtocolCodegenType.Json
 import app.cash.redwood.tooling.codegen.ProtocolCodegenType.Widget
-import app.cash.redwood.tooling.schema.ProtocolSchema
+import app.cash.redwood.tooling.schema.ProtocolSchemaSet
 import app.cash.redwood.tooling.schema.toEmbeddedSchema
 import java.nio.file.Path
 import kotlin.io.path.createDirectories
@@ -30,21 +30,21 @@ public enum class ProtocolCodegenType {
   Widget,
 }
 
-public fun ProtocolSchema.generate(type: ProtocolCodegenType, destination: Path) {
+public fun ProtocolSchemaSet.generate(type: ProtocolCodegenType, destination: Path) {
   when (type) {
     Compose -> {
       generateProtocolBridge(this).writeTo(destination)
       generateProtocolLayoutModifierSerialization(this).writeTo(destination)
-      for (dependency in allSchemas) {
-        generateProtocolWidgetFactory(dependency, host = this).writeTo(destination)
-        generateProtocolLayoutModifierSerializers(dependency, host = this)?.writeTo(destination)
+      for (dependency in all) {
+        generateProtocolWidgetFactory(dependency, host = schema).writeTo(destination)
+        generateProtocolLayoutModifierSerializers(dependency, host = schema)?.writeTo(destination)
         for (widget in dependency.widgets) {
-          generateProtocolWidget(dependency, widget, host = this).writeTo(destination)
+          generateProtocolWidget(dependency, widget, host = schema).writeTo(destination)
         }
       }
     }
     Json -> {
-      val embeddedSchema = toEmbeddedSchema()
+      val embeddedSchema = schema.toEmbeddedSchema()
       val path = destination.resolve(embeddedSchema.path)
       path.parent.createDirectories()
       path.writeText(embeddedSchema.json)
@@ -52,10 +52,10 @@ public fun ProtocolSchema.generate(type: ProtocolCodegenType, destination: Path)
     Widget -> {
       generateDiffConsumingNodeFactory(this).writeTo(destination)
       generateDiffConsumingLayoutModifierSerialization(this).writeTo(destination)
-      for (dependency in allSchemas) {
-        generateDiffConsumingLayoutModifierImpls(dependency, host = this).writeTo(destination)
+      for (dependency in all) {
+        generateDiffConsumingLayoutModifierImpls(dependency, host = schema).writeTo(destination)
         for (widget in dependency.widgets) {
-          generateDiffConsumingWidget(dependency, widget, host = this).writeTo(destination)
+          generateDiffConsumingWidget(dependency, widget, host = schema).writeTo(destination)
         }
       }
     }
diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/sharedHelpers.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/sharedHelpers.kt
index cb6930e05..7569f1730 100644
--- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/sharedHelpers.kt
+++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/sharedHelpers.kt
@@ -19,6 +19,7 @@ import app.cash.redwood.tooling.schema.FqType
 import app.cash.redwood.tooling.schema.LayoutModifier
 import app.cash.redwood.tooling.schema.ProtocolLayoutModifier
 import app.cash.redwood.tooling.schema.ProtocolSchema
+import app.cash.redwood.tooling.schema.ProtocolSchemaSet
 import app.cash.redwood.tooling.schema.Schema
 import app.cash.redwood.tooling.schema.Widget
 import app.cash.redwood.tooling.schema.Widget.Event
@@ -85,35 +86,35 @@ internal fun Schema.diffConsumingNodeType(widget: Widget, host: Schema): ClassNa
 }
 
 internal fun Schema.widgetType(widget: Widget): ClassName {
-  return ClassName(widgetPackage(this), widget.type.flatName)
+  return ClassName(widgetPackage(), widget.type.flatName)
 }
 
 internal fun Schema.getWidgetFactoryType(): ClassName {
-  return ClassName(widgetPackage(this), "${type.flatName}WidgetFactory")
+  return ClassName(widgetPackage(), "${type.flatName}WidgetFactory")
 }
 
 internal fun Schema.getMutableWidgetFactoryType(): ClassName {
-  return ClassName(widgetPackage(this), "Mutable${type.flatName}WidgetFactory")
+  return ClassName(widgetPackage(), "Mutable${type.flatName}WidgetFactory")
 }
 
 internal fun Schema.mutableWidgetType(widget: Widget): ClassName {
-  return ClassName(widgetPackage(this), "Mutable${widget.type.flatName}")
+  return ClassName(widgetPackage(), "Mutable${widget.type.flatName}")
 }
 
 internal fun Schema.widgetValueType(widget: Widget): ClassName {
-  return ClassName(widgetPackage(this), "${widget.type.flatName}Value")
+  return ClassName(widgetPackage(), "${widget.type.flatName}Value")
 }
 
 internal fun Schema.getWidgetFactoryProviderType(): ClassName {
-  return ClassName(widgetPackage(this), "${type.flatName}WidgetFactoryProvider")
+  return ClassName(widgetPackage(), "${type.flatName}WidgetFactoryProvider")
 }
 
 internal fun Schema.getWidgetFactoriesType(): ClassName {
-  return ClassName(widgetPackage(this), "${type.flatName}WidgetFactories")
+  return ClassName(widgetPackage(), "${type.flatName}WidgetFactories")
 }
 
-internal fun Schema.widgetPackage(host: Schema = this): String {
-  return if (this === host) {
+internal fun Schema.widgetPackage(host: Schema? = null): String {
+  return if (host == null) {
     val `package` = type.names[0]
     "$`package`.widget"
   } else {
@@ -135,17 +136,17 @@ internal fun Schema.layoutModifierImpl(layoutModifier: LayoutModifier): ClassNam
 }
 
 internal fun Schema.getTesterFunction(): MemberName {
-  return MemberName(widgetPackage(this), "${type.flatName}Tester")
+  return MemberName(widgetPackage(), "${type.flatName}Tester")
 }
 
 internal val Schema.toLayoutModifier: MemberName get() =
-  MemberName(widgetPackage(this), "toLayoutModifier")
+  MemberName(widgetPackage(), "toLayoutModifier")
 
 internal val Schema.layoutModifierToProtocol: MemberName get() =
   MemberName(composePackage(), "toProtocol")
 
-internal fun ProtocolSchema.allLayoutModifiers(): List<Pair<ProtocolSchema, ProtocolLayoutModifier>> {
-  return allSchemas.flatMap { schema ->
+internal fun ProtocolSchemaSet.allLayoutModifiers(): List<Pair<ProtocolSchema, ProtocolLayoutModifier>> {
+  return all.flatMap { schema ->
     schema.layoutModifiers.map { schema to it }
   }
 }
diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/widgetGeneration.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/widgetGeneration.kt
index 4965ccb00..f4343ad2d 100644
--- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/widgetGeneration.kt
+++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/widgetGeneration.kt
@@ -18,6 +18,7 @@ package app.cash.redwood.tooling.codegen
 import app.cash.redwood.tooling.schema.ProtocolWidget
 import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolTrait
 import app.cash.redwood.tooling.schema.Schema
+import app.cash.redwood.tooling.schema.SchemaSet
 import app.cash.redwood.tooling.schema.Widget
 import app.cash.redwood.tooling.schema.Widget.Children
 import app.cash.redwood.tooling.schema.Widget.Event
@@ -41,7 +42,8 @@ interface SunspotWidgetFactoryProvider<W : Any> : RedwoodLayoutWidgetFactoryProv
   val Sunspot: SunspotWidgetFactory<W>
 }
  */
-internal fun generateWidgetFactories(schema: Schema): FileSpec {
+internal fun generateWidgetFactories(schemaSet: SchemaSet): FileSpec {
+  val schema = schemaSet.schema
   val widgetFactoriesType = schema.getWidgetFactoriesType()
   return FileSpec.builder(widgetFactoriesType.packageName, widgetFactoriesType.simpleName)
     .addType(
@@ -51,7 +53,7 @@ internal fun generateWidgetFactories(schema: Schema): FileSpec {
         .apply {
           val constructorBuilder = FunSpec.constructorBuilder()
 
-          for (dependency in schema.allSchemas) {
+          for (dependency in schemaSet.all) {
             val dependencyType = dependency.getWidgetFactoryType().parameterizedBy(typeVariableW)
             addProperty(
               PropertySpec.builder(dependency.type.flatName, dependencyType, OVERRIDE)
@@ -71,7 +73,7 @@ internal fun generateWidgetFactories(schema: Schema): FileSpec {
         .addSuperinterface(RedwoodWidget.WidgetProvider.parameterizedBy(typeVariableW))
         .addProperty(schema.type.flatName, schema.getWidgetFactoryType().parameterizedBy(typeVariableW))
         .apply {
-          for (dependency in schema.dependencies) {
+          for (dependency in schemaSet.dependencies.values) {
             addSuperinterface(dependency.getWidgetFactoryProviderType().parameterizedBy(typeVariableW))
           }
         }
diff --git a/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schema.kt b/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schema.kt
index 9f3f133a5..3eb750394 100644
--- a/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schema.kt
+++ b/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schema.kt
@@ -20,18 +20,23 @@ import app.cash.redwood.tooling.schema.Widget.Event
 import app.cash.redwood.tooling.schema.Widget.Property
 import app.cash.redwood.tooling.schema.Widget.Trait
 
+/** A [Schema] and its dependencies. */
+public interface SchemaSet {
+  public val schema: Schema
+  public val dependencies: Map<FqType, Schema>
+
+  public val all: List<Schema> get() = buildList {
+    add(schema)
+    addAll(dependencies.values)
+  }
+}
+
 public interface Schema {
   public val type: FqType
   public val scopes: List<FqType>
   public val widgets: List<Widget>
   public val layoutModifiers: List<LayoutModifier>
-  public val dependencies: List<Schema>
-
-  /** This schema and all its [dependencies]. */
-  public val allSchemas: List<Schema> get() = buildList {
-    add(this@Schema)
-    addAll(dependencies)
-  }
+  public val dependencies: List<FqType>
 }
 
 public interface Widget {
@@ -76,15 +81,20 @@ public interface LayoutModifier {
   )
 }
 
+/** A [ProtocolSchema] and its dependencies. */
+public interface ProtocolSchemaSet : SchemaSet {
+  override val schema: ProtocolSchema
+  override val dependencies: Map<FqType, ProtocolSchema>
+
+  override val all: List<ProtocolSchema> get() = buildList {
+    add(schema)
+    addAll(dependencies.values)
+  }
+}
+
 public interface ProtocolSchema : Schema {
   override val widgets: List<ProtocolWidget>
   override val layoutModifiers: List<ProtocolLayoutModifier>
-  override val dependencies: List<ProtocolSchema>
-
-  override val allSchemas: List<ProtocolSchema> get() = buildList {
-    add(this@ProtocolSchema)
-    addAll(dependencies)
-  }
 }
 
 public interface ProtocolWidget : Widget {
diff --git a/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaClasses.kt b/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaClasses.kt
index e642b35b2..60c0ddf34 100644
--- a/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaClasses.kt
+++ b/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaClasses.kt
@@ -21,12 +21,17 @@ import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolEvent
 import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolProperty
 import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolTrait
 
+internal data class ParsedProtocolSchemaSet(
+  override val schema: ProtocolSchema,
+  override val dependencies: Map<FqType, ProtocolSchema>,
+) : ProtocolSchemaSet
+
 internal data class ParsedProtocolSchema(
   override val type: FqType,
   override val scopes: List<FqType>,
   override val widgets: List<ProtocolWidget>,
   override val layoutModifiers: List<ProtocolLayoutModifier>,
-  override val dependencies: List<ProtocolSchema>,
+  override val dependencies: List<FqType>,
 ) : ProtocolSchema
 
 internal data class ParsedProtocolWidget(
diff --git a/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaJson.kt b/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaJson.kt
index bdc11e907..cb07f031e 100644
--- a/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaJson.kt
+++ b/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaJson.kt
@@ -86,7 +86,7 @@ public fun ProtocolSchema.toEmbeddedSchema(): EmbeddedSchema {
         },
       )
     },
-    dependencies = dependencies.map { it.type },
+    dependencies = dependencies,
   )
   val json = json.encodeToString(SchemaJsonV1.serializer(), schemaJson)
 
diff --git a/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaParser.kt b/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaParser.kt
index b5c309d28..d29c366ae 100644
--- a/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaParser.kt
+++ b/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaParser.kt
@@ -41,11 +41,11 @@ private val KClass<*>.schemaAnnotation: SchemaAnnotation get() {
   return requireNotNull(findAnnotation()) { "Schema $qualifiedName missing @Schema annotation" }
 }
 
-public fun parseSchema(schemaType: KClass<*>): Schema {
+public fun parseSchema(schemaType: KClass<*>): SchemaSet {
   return parseProtocolSchema(schemaType)
 }
 
-public fun parseProtocolSchema(schemaType: KClass<*>, tag: Int = 0): ProtocolSchema {
+public fun parseProtocolSchema(schemaType: KClass<*>, tag: Int = 0): ProtocolSchemaSet {
   val schemaAnnotation = schemaType.schemaAnnotation
   require(tag in 0..maxSchemaTag) { "Schema tag must be in range [0, $maxSchemaTag]: $tag" }
 
@@ -154,7 +154,7 @@ public fun parseProtocolSchema(schemaType: KClass<*>, tag: Int = 0): ProtocolSch
       require(it.tag in 1..maxSchemaTag) {
         "Dependency ${it.schema.qualifiedName} tag must be in range (0, $maxSchemaTag]: ${it.tag}"
       }
-      val schema = parseProtocolSchema(it.schema, it.tag)
+      val schema = parseProtocolSchema(it.schema, it.tag).schema
       require(schema.dependencies.isEmpty()) {
         "Schema dependency ${it.schema.qualifiedName} also has its own dependencies. " +
           "For now, only a single level of dependencies is supported."
@@ -167,10 +167,14 @@ public fun parseProtocolSchema(schemaType: KClass<*>, tag: Int = 0): ProtocolSch
     scopes.toList(),
     widgets,
     layoutModifiers,
-    dependencies,
+    dependencies.map { it.type },
+  )
+  val schemaSet = ParsedProtocolSchemaSet(
+    schema,
+    dependencies.associateBy { it.type },
   )
 
-  val duplicatedWidgets = (listOf(schema) + dependencies)
+  val duplicatedWidgets = schemaSet.all
     .flatMap { it.widgets.map { widget -> widget to it } }
     .groupBy { it.first.type }
     .filterValues { it.size > 1 }
@@ -187,7 +191,7 @@ public fun parseProtocolSchema(schemaType: KClass<*>, tag: Int = 0): ProtocolSch
     )
   }
 
-  return schema
+  return schemaSet
 }
 
 private fun parseWidget(


Test patch:
diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/testingGeneration.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/testingGeneration.kt
index eb58577a9..c6cda4114 100644
--- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/testingGeneration.kt
+++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/testingGeneration.kt
@@ -17,6 +17,7 @@ package app.cash.redwood.tooling.codegen
 
 import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolTrait
 import app.cash.redwood.tooling.schema.Schema
+import app.cash.redwood.tooling.schema.SchemaSet
 import app.cash.redwood.tooling.schema.Widget
 import app.cash.redwood.tooling.schema.Widget.Children
 import app.cash.redwood.tooling.schema.Widget.Event
@@ -49,7 +50,8 @@ public fun SunspotTester(scope: CoroutineScope): RedwoodTester {
   )
 }
 */
-internal fun generateTester(schema: Schema): FileSpec {
+internal fun generateTester(schemaSet: SchemaSet): FileSpec {
+  val schema = schemaSet.schema
   val testerFunction = schema.getTesterFunction()
   return FileSpec.builder(testerFunction.packageName, testerFunction.simpleName)
     .addFunction(
@@ -61,7 +63,7 @@ internal fun generateTester(schema: Schema): FileSpec {
         .addCode("scope = scope,\n")
         .addCode("provider = %T(⇥\n", schema.getWidgetFactoriesType())
         .apply {
-          for (dependency in schema.allSchemas) {
+          for (dependency in schemaSet.all) {
             addCode("%N = %T(),\n", dependency.type.flatName, dependency.getMutableWidgetFactoryType())
           }
         }
diff --git a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ComposeGenerationTest.kt b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ComposeGenerationTest.kt
index edea0bc23..9949bfd7a 100644
--- a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ComposeGenerationTest.kt
+++ b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ComposeGenerationTest.kt
@@ -43,7 +43,7 @@ class ComposeGenerationTest {
   )
 
   @Test fun `scoped and unscoped children`() {
-    val schema = parseSchema(ScopedAndUnscopedSchema::class)
+    val schema = parseSchema(ScopedAndUnscopedSchema::class).schema
 
     val fileSpec = generateComposable(schema, schema.widgets.single())
     assertThat(fileSpec.toString()).apply {
@@ -53,7 +53,7 @@ class ComposeGenerationTest {
   }
 
   @Test fun `scope is annotated with layout scope marker`() {
-    val schema = parseSchema(ScopedAndUnscopedSchema::class)
+    val schema = parseSchema(ScopedAndUnscopedSchema::class).schema
 
     val fileSpec = generateScope(schema, FqType(listOf("example", "RowScope")))
     assertThat(fileSpec.toString()).contains(
@@ -85,7 +85,7 @@ class ComposeGenerationTest {
   )
 
   @Test fun `default is supported for all property types`() {
-    val schema = parseSchema(DefaultSchema::class)
+    val schema = parseSchema(DefaultSchema::class).schema
 
     val fileSpec = generateComposable(schema, schema.widgets.single())
     assertThat(fileSpec.toString()).apply {
@@ -109,7 +109,7 @@ class ComposeGenerationTest {
   )
 
   @Test fun `layout modifier is the last non child parameter`() {
-    val schema = parseSchema(MultipleChildSchema::class)
+    val schema = parseSchema(MultipleChildSchema::class).schema
 
     val fileSpec = generateComposable(schema, schema.widgets.single())
     assertThat(fileSpec.toString()).contains(
diff --git a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ComposeProtocolGenerationTest.kt b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ComposeProtocolGenerationTest.kt
index b4a9aaf5f..b91783bad 100644
--- a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ComposeProtocolGenerationTest.kt
+++ b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ComposeProtocolGenerationTest.kt
@@ -37,7 +37,7 @@ class ComposeProtocolGenerationTest {
   )
 
   @Test fun `id property does not collide`() {
-    val schema = parseProtocolSchema(IdPropertyNameCollisionSchema::class)
+    val schema = parseProtocolSchema(IdPropertyNameCollisionSchema::class).schema
 
     val fileSpec = generateProtocolWidget(schema, schema.widgets.single())
     assertThat(fileSpec.toString()).contains(
@@ -49,9 +49,9 @@ class ComposeProtocolGenerationTest {
   }
 
   @Test fun `dependency layout modifiers are included in serialization`() {
-    val schema = parseProtocolSchema(PrimarySchema::class)
+    val schemaSet = parseProtocolSchema(PrimarySchema::class)
 
-    val fileSpec = generateProtocolLayoutModifierSerialization(schema)
+    val fileSpec = generateProtocolLayoutModifierSerialization(schemaSet)
     assertThat(fileSpec.toString()).apply {
       contains("is PrimaryModifier -> PrimaryModifierSerializer.encode(json, this)")
       contains("is SecondaryModifier -> SecondaryModifierSerializer.encode(json, this)")
diff --git a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/LayoutModifierGenerationTest.kt b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/LayoutModifierGenerationTest.kt
index 363aff119..c0dd6f67d 100644
--- a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/LayoutModifierGenerationTest.kt
+++ b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/LayoutModifierGenerationTest.kt
@@ -44,7 +44,7 @@ class LayoutModifierGenerationTest {
   data class ContentDescription(val text: String)
 
   @Test fun `simple names do not collide`() {
-    val schema = parseSchema(SimpleNameCollisionSchema::class)
+    val schema = parseSchema(SimpleNameCollisionSchema::class).schema
 
     val topType = schema.layoutModifiers.single { it.type.flatName == "LayoutModifierGenerationTestContentDescription" }
     val topTypeSpec = generateLayoutModifierInterface(schema, topType)
@@ -68,7 +68,7 @@ class LayoutModifierGenerationTest {
   object ScopedLayoutModifier
 
   @Test fun `layout modifier functions are stable`() {
-    val schema = parseSchema(ScopedModifierSchema::class)
+    val schema = parseSchema(ScopedModifierSchema::class).schema
 
     val modifier = schema.layoutModifiers.single { it.type.names.last() == "ScopedLayoutModifier" }
     val scope = modifier.scopes.single { it.names.last() == "LayoutModifierScope" }
diff --git a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/TestingGenerationTest.kt b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/TestingGenerationTest.kt
index b6336f491..6ff468600 100644
--- a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/TestingGenerationTest.kt
+++ b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/TestingGenerationTest.kt
@@ -46,9 +46,10 @@ class TestingGenerationTest {
   )
 
   @Test fun `tester happy path`() {
-    val schema = parseSchema(HappyPathSchema::class)
+    val schemaSet = parseSchema(HappyPathSchema::class)
+    val schema = schemaSet.schema
 
-    val testerFileSpec = generateTester(schema)
+    val testerFileSpec = generateTester(schemaSet)
     assertThat(testerFileSpec.toString()).contains(
       """
       |@OptIn(RedwoodCodegenApi::class)
diff --git a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/WidgetGenerationTest.kt b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/WidgetGenerationTest.kt
index b7283e87e..6cf516074 100644
--- a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/WidgetGenerationTest.kt
+++ b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/WidgetGenerationTest.kt
@@ -39,7 +39,7 @@ class WidgetGenerationTest {
   data class Button(@Property(1) val text: String)
 
   @Test fun `simple names do not collide`() {
-    val schema = parseSchema(SimpleNameCollisionSchema::class)
+    val schema = parseSchema(SimpleNameCollisionSchema::class).schema
 
     val fileSpec = generateWidgetFactory(schema)
     assertThat(fileSpec.toString()).apply {
@@ -49,7 +49,7 @@ class WidgetGenerationTest {
   }
 
   @Test fun tagInWidgetFactoryKDoc() {
-    val schema = parseSchema(SimpleNameCollisionSchema::class)
+    val schema = parseSchema(SimpleNameCollisionSchema::class).schema
 
     val fileSpec = generateWidgetFactory(schema)
     assertThat(fileSpec.toString()).contains(
@@ -62,7 +62,7 @@ class WidgetGenerationTest {
   }
 
   @Test fun tagInWidgetKdoc() {
-    val schema = parseSchema(SimpleNameCollisionSchema::class)
+    val schema = parseSchema(SimpleNameCollisionSchema::class).schema
     val button = schema.widgets.single { it.type.flatName == "WidgetGenerationTestButton" }
 
     val fileSpec = generateWidget(schema, button)
diff --git a/redwood-tooling-schema/src/test/kotlin/app/cash/redwood/tooling/schema/SchemaParserTest.kt b/redwood-tooling-schema/src/test/kotlin/app/cash/redwood/tooling/schema/SchemaParserTest.kt
index c3e78f373..d49d155b9 100644
--- a/redwood-tooling-schema/src/test/kotlin/app/cash/redwood/tooling/schema/SchemaParserTest.kt
+++ b/redwood-tooling-schema/src/test/kotlin/app/cash/redwood/tooling/schema/SchemaParserTest.kt
@@ -406,7 +406,7 @@ class SchemaParserTest {
   )
 
   @Test fun eventTypes() {
-    val schema = parseProtocolSchema(EventTypeSchema::class)
+    val schema = parseProtocolSchema(EventTypeSchema::class).schema
     val widget = schema.widgets.single()
     assertThat(widget.traits.single { it.name == "requiredEvent" }).isInstanceOf<Event>()
     assertThat(widget.traits.single { it.name == "optionalEvent" }).isInstanceOf<Event>()
@@ -427,7 +427,7 @@ class SchemaParserTest {
   )
 
   @Test fun eventArguments() {
-    val schema = parseProtocolSchema(EventArgumentsSchema::class)
+    val schema = parseProtocolSchema(EventArgumentsSchema::class).schema
     val widget = schema.widgets.single()
 
     val noArguments = widget.traits.single { it.name == "noArguments" } as Event
@@ -470,7 +470,7 @@ class SchemaParserTest {
   object ObjectWidget
 
   @Test fun objectWidget() {
-    val schema = parseProtocolSchema(ObjectSchema::class)
+    val schema = parseProtocolSchema(ObjectSchema::class).schema
     val widget = schema.widgets.single()
     assertThat(widget.traits).isEmpty()
   }
@@ -570,7 +570,7 @@ class SchemaParserTest {
   )
 
   @Test fun schemaTagDefault() {
-    val schema = parseProtocolSchema(SchemaTag::class)
+    val schema = parseProtocolSchema(SchemaTag::class).schema
 
     val widget = schema.widgets.single()
     assertThat(widget.tag).isEqualTo(1)
@@ -591,7 +591,7 @@ class SchemaParserTest {
 
   @Test fun schemaTagOffsetsMemberTags() {
     val schema = parseProtocolSchema(SchemaDependencyTagOffsetsMemberTags::class)
-    val dependency = schema.dependencies.single()
+    val dependency = schema.dependencies.values.single()
 
     val widget = dependency.widgets.single()
     assertThat(widget.tag).isEqualTo(4_000_001)