Repository: cashapp/redwood
Pull request #476 (2022-10-25 04:01:52)

Remove unscoped layout modifiers.
Enforces that layout modifiers have at least one scope.


Patch:
diff --git a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/GenerateCommand.kt b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/GenerateCommand.kt
index f8637789c..63402adee 100644
--- a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/GenerateCommand.kt
+++ b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/GenerateCommand.kt
@@ -71,19 +71,17 @@ internal class GenerateCommand : CliktCommand(name = "generate") {
     when (type) {
       Compose -> {
         generateComposableTargetMarker(schema).writeTo(out)
-        generateUnscopedModifierFunctions(schema)?.writeTo(out)
-        generateModifierImpls(schema)?.writeTo(out)
+        generateLayoutModifierImpls(schema)?.writeTo(out)
         for (scope in schema.scopes) {
-          generateScopeAndScopedModifiers(schema, scope).writeTo(out)
+          generateScope(schema, scope).writeTo(out)
         }
         for (widget in schema.widgets) {
           generateComposable(schema, widget).writeTo(out)
         }
         for (dependency in schema.dependencies) {
-          generateUnscopedModifierFunctions(dependency)?.writeTo(out)
-          generateModifierImpls(dependency)?.writeTo(out)
+          generateLayoutModifierImpls(dependency)?.writeTo(out)
           for (scope in dependency.scopes) {
-            generateScopeAndScopedModifiers(dependency, scope).writeTo(out)
+            generateScope(dependency, scope).writeTo(out)
           }
           for (widget in dependency.widgets) {
             generateComposable(dependency, widget, host = schema).writeTo(out)
diff --git a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/composeGeneration.kt b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/composeGeneration.kt
index acfc2dfdd..27d2aea3a 100644
--- a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/composeGeneration.kt
+++ b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/composeGeneration.kt
@@ -29,7 +29,6 @@ import com.squareup.kotlinpoet.FunSpec
 import com.squareup.kotlinpoet.KModifier.INTERNAL
 import com.squareup.kotlinpoet.KModifier.OVERRIDE
 import com.squareup.kotlinpoet.KModifier.PUBLIC
-import com.squareup.kotlinpoet.KModifier.SEALED
 import com.squareup.kotlinpoet.ParameterSpec
 import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
 import com.squareup.kotlinpoet.PropertySpec
@@ -219,7 +218,7 @@ internal fun generateComposable(
 }
 
 /*
-sealed interface RowScope {
+interface RowScope {
   @Stable
   fun LayoutModifier.something(...): LayoutModifier {
     return then(SomethingImpl(...))
@@ -228,14 +227,13 @@ sealed interface RowScope {
 
 internal object RowScopeImpl : RowScope
 */
-internal fun generateScopeAndScopedModifiers(schema: Schema, scope: KClass<*>): FileSpec {
+internal fun generateScope(schema: Schema, scope: KClass<*>): FileSpec {
   val scopeName = scope.simpleName!!
   val scopeType = ClassName(schema.composePackage(), scopeName)
   return FileSpec.builder(scopeType.packageName, scopeType.simpleName)
     .apply {
       val scopeBuilder = TypeSpec.interfaceBuilder(scopeType)
         .addAnnotation(Redwood.LayoutScopeMarker)
-        .addModifiers(SEALED)
 
       for (layoutModifier in schema.layoutModifiers) {
         if (scope !in layoutModifier.scopes) {
@@ -256,25 +254,6 @@ internal fun generateScopeAndScopedModifiers(schema: Schema, scope: KClass<*>):
     .build()
 }
 
-/*
-@Stable
-fun LayoutModifier.something(...): LayoutModifier {
-  return then(SomethingImpl(...))
-}
-*/
-internal fun generateUnscopedModifierFunctions(schema: Schema): FileSpec? {
-  val unscopedLayoutModifiers = schema.layoutModifiers.filter { it.scopes.isEmpty() }
-  if (unscopedLayoutModifiers.isEmpty()) return null
-
-  return FileSpec.builder(schema.composePackage(), "unscopedLayoutModifiers")
-    .apply {
-      for (layoutModifier in unscopedLayoutModifiers) {
-        addFunction(generateLayoutModifierFunction(schema, layoutModifier))
-      }
-    }
-    .build()
-}
-
 /*
 internal class SomethingImpl(...): Something {
   public override fun equals(other: Any?): Boolean = ...
@@ -282,7 +261,7 @@ internal class SomethingImpl(...): Something {
   public override fun toString(): String = ...
 }
 */
-internal fun generateModifierImpls(schema: Schema): FileSpec? {
+internal fun generateLayoutModifierImpls(schema: Schema): FileSpec? {
   if (schema.layoutModifiers.isEmpty()) return null
 
   return FileSpec.builder(schema.composePackage(), "layoutModifiers")
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 5d86dc0e9..7179bce7d 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
@@ -116,11 +116,11 @@ public annotation class Default(val expression: String)
 
 /**
  * Annotates a data class which represents a layout modifier for a [Widget]. Each layout modifier
- * in a [Schema] must have a unique [tag] among all [@LayoutModifier][LayoutModifier] annotations
- * in the [Schema].
+ * in a [Schema] must have a unique [tag] among all [LayoutModifier] annotations in the [Schema].
+ * Additionally, each layout modifier must be associated with at least one scope.
  *
  * ```
- * @LayoutModifier(1)
+ * @LayoutModifier(1, RowScope::class)
  * data class RowAlignment(
  *   val value: VerticalAlignment,
  * )
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 d8a7d76b8..0b4cbbd56 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
@@ -82,8 +82,8 @@ public fun parseSchema(schemaType: KClass<*>, tag: UInt = 0U): Schema {
     throw IllegalArgumentException(
       buildString {
         appendLine("Schema @Widget tags must be unique")
-        for ((tag, group) in badWidgets) {
-          append("\n- @Widget($tag): ")
+        for ((widgetTag, group) in badWidgets) {
+          append("\n- @Widget($widgetTag): ")
           group.joinTo(this) { it.type.qualifiedName!! }
         }
       },
@@ -95,18 +95,24 @@ public fun parseSchema(schemaType: KClass<*>, tag: UInt = 0U): Schema {
     throw IllegalArgumentException(
       buildString {
         appendLine("Schema @LayoutModifier tags must be unique")
-        for ((tag, group) in badLayoutModifiers) {
-          append("\n- @LayoutModifier($tag): ")
+        for ((modifierTag, group) in badLayoutModifiers) {
+          append("\n- @LayoutModifier($modifierTag): ")
           group.joinTo(this) { it.type.qualifiedName!! }
         }
       },
     )
   }
 
-  val scopes = widgets
+  val widgetScopes = widgets
     .flatMap { it.traits }
     .filterIsInstance<Widget.Children>()
-    .mapNotNullTo(mutableSetOf()) { it.scope }
+    .mapNotNull { it.scope }
+  val layoutModifierScopes = layoutModifiers
+    .flatMap { it.scopes }
+  val scopes = buildSet {
+    addAll(widgetScopes)
+    addAll(layoutModifierScopes)
+  }
 
   val badDependencyTags = schemaAnnotation.dependencies
     .groupBy { it.tag }
@@ -115,8 +121,8 @@ public fun parseSchema(schemaType: KClass<*>, tag: UInt = 0U): Schema {
     throw IllegalArgumentException(
       buildString {
         appendLine("Schema dependency tags must be unique")
-        for ((tag, group) in badDependencyTags) {
-          append("\n- Dependency tag $tag: ")
+        for ((dependencyTag, group) in badDependencyTags) {
+          append("\n- Dependency tag $dependencyTag: ")
           group.joinTo(this) { it.schema.qualifiedName!! }
         }
       },
@@ -243,8 +249,8 @@ private fun parseWidget(
     throw IllegalArgumentException(
       buildString {
         appendLine("${memberType.qualifiedName}'s @Children tags must be unique")
-        for ((tag, group) in badChildren) {
-          append("\n- @Children($tag): ")
+        for ((childTag, group) in badChildren) {
+          append("\n- @Children($childTag): ")
           group.joinTo(this) { it.name }
         }
       },
@@ -258,8 +264,8 @@ private fun parseWidget(
     throw IllegalArgumentException(
       buildString {
         appendLine("${memberType.qualifiedName}'s @Property tags must be unique")
-        for ((tag, group) in badProperties) {
-          append("\n- @Property($tag): ")
+        for ((propertyTag, group) in badProperties) {
+          append("\n- @Property($propertyTag): ")
           group.joinTo(this) { it.name }
         }
       },
@@ -277,6 +283,9 @@ private fun parseLayoutModifier(
   require(annotation.tag in 1 until maxMemberTag.toInt()) {
     "@LayoutModifier ${memberType.qualifiedName} tag must be in range [1, $maxMemberTag): ${annotation.tag}"
   }
+  require(annotation.scopes.isNotEmpty()) {
+    "@LayoutModifier ${memberType.qualifiedName} must have at least one scope."
+  }
   val tag = schemaTag * maxMemberTag + annotation.tag.toUIntExact()
 
   val properties = if (memberType.isData) {


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 eb87a54da..6786d66d7 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
@@ -63,11 +63,11 @@ class ComposeGenerationTest {
   @Test fun scopeIsAnnotatedWithLayoutScopeMarker() {
     val schema = parseSchema(ScopedAndUnscopedSchema::class)
 
-    val fileSpec = generateScopeAndScopedModifiers(schema, RowScope::class)
+    val fileSpec = generateScope(schema, RowScope::class)
     assertThat(fileSpec.toString()).contains(
       """
       |@LayoutScopeMarker
-      |public sealed interface RowScope
+      |public interface RowScope
       """.trimMargin(),
     )
   }
diff --git a/redwood-cli/src/test/kotlin/app/cash/redwood/generator/LayoutModifierGenerationTest.kt b/redwood-cli/src/test/kotlin/app/cash/redwood/generator/LayoutModifierGenerationTest.kt
index 71aaa31b4..40e74f33c 100644
--- a/redwood-cli/src/test/kotlin/app/cash/redwood/generator/LayoutModifierGenerationTest.kt
+++ b/redwood-cli/src/test/kotlin/app/cash/redwood/generator/LayoutModifierGenerationTest.kt
@@ -19,9 +19,7 @@ import app.cash.redwood.schema.LayoutModifier
 import app.cash.redwood.schema.Schema
 import app.cash.redwood.schema.parser.parseSchema
 import com.google.common.truth.Truth.assertThat
-import example.redwood.compose.customType
-import example.redwood.compose.customTypeStateless
-import example.redwood.compose.customTypeWithDefault
+import example.redwood.compose.TestScope
 import kotlin.time.Duration.Companion.minutes
 import kotlin.time.Duration.Companion.seconds
 import org.junit.Test
@@ -35,12 +33,14 @@ class LayoutModifierGenerationTest {
   )
   interface SimpleNameCollisionSchema
 
+  object SimpleNameCollisionScope
+
   interface NavigationBar {
-    @LayoutModifier(1)
+    @LayoutModifier(1, SimpleNameCollisionScope::class)
     data class ContentDescription(val text: String)
   }
 
-  @LayoutModifier(3)
+  @LayoutModifier(2, SimpleNameCollisionScope::class)
   data class ContentDescription(val text: String)
 
   @Test fun `simple names do not collide`() {
@@ -58,42 +58,30 @@ class LayoutModifierGenerationTest {
   @Schema(
     [
       ScopedLayoutModifier::class,
-      UnscopedLayoutModifier::class,
     ],
   )
-  interface ScopedAndUnscopedSchema
+  interface ScopedModifierSchema
 
   object LayoutModifierScope
 
   @LayoutModifier(1, LayoutModifierScope::class)
   object ScopedLayoutModifier
 
-  @LayoutModifier(2)
-  object UnscopedLayoutModifier
-
   @Test fun `layout modifier functions are stable`() {
-    val schema = parseSchema(ScopedAndUnscopedSchema::class)
+    val schema = parseSchema(ScopedModifierSchema::class)
 
-    val scopedModifier = schema.layoutModifiers.single { it.type == ScopedLayoutModifier::class }
-    val scope = scopedModifier.scopes.single { it == LayoutModifierScope::class }
-    val scopeSpec = generateScopeAndScopedModifiers(schema, scope)
+    val modifier = schema.layoutModifiers.single { it.type == ScopedLayoutModifier::class }
+    val scope = modifier.scopes.single { it == LayoutModifierScope::class }
+    val scopeSpec = generateScope(schema, scope)
     assertThat(scopeSpec.toString()).contains(
       """
       |  @Stable
       |  public fun LayoutModifier.scopedLayoutModifier(): LayoutModifier
       """.trimMargin(),
     )
-
-    val unscopedModifierSpec = generateUnscopedModifierFunctions(schema)
-    assertThat(unscopedModifierSpec.toString()).contains(
-      """
-      |@Stable
-      |public fun LayoutModifier.unscopedLayoutModifier(): LayoutModifier
-      """.trimMargin(),
-    )
   }
 
-  @Test fun `layout modifier implements toString`() {
+  @Test fun `layout modifier implements toString`() = with(object : TestScope {}) {
     var type = app.cash.redwood.LayoutModifier.customType(20.seconds)
     assertThat(type.toString()).isEqualTo("CustomType(customType=20s)")
 
diff --git a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetFactoryTest.kt b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetFactoryTest.kt
index f2bee8d66..26fc97e64 100644
--- a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetFactoryTest.kt
+++ b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetFactoryTest.kt
@@ -22,8 +22,7 @@ import app.cash.redwood.protocol.Id
 import app.cash.redwood.protocol.LayoutModifiers
 import app.cash.redwood.protocol.PropertyDiff
 import example.redwood.compose.DiffProducingExampleSchemaWidgetFactory
-import example.redwood.compose.customType
-import example.redwood.compose.customTypeWithDefault
+import example.redwood.compose.TestScope
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
@@ -77,7 +76,9 @@ class DiffProducingWidgetFactoryTest {
     diffProducingWidget.id = Id(1U)
     diffProducingWidget._diffAppender = diffAppender
 
-    button.layoutModifiers = LayoutModifier.customType(10.seconds)
+    button.layoutModifiers = with(object : TestScope {}) {
+      LayoutModifier.customType(10.seconds)
+    }
     diffAppender.trySend()
 
     val expected = Diff(
@@ -117,7 +118,9 @@ class DiffProducingWidgetFactoryTest {
     diffProducingWidget.id = Id(1U)
     diffProducingWidget._diffAppender = diffAppender
 
-    button.layoutModifiers = LayoutModifier.customTypeWithDefault(10.seconds, "sup")
+    button.layoutModifiers = with(object : TestScope {}) {
+      LayoutModifier.customTypeWithDefault(10.seconds, "sup")
+    }
     diffAppender.trySend()
 
     val expected = Diff(
diff --git a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidgetFactoryTest.kt b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidgetFactoryTest.kt
index 65f383687..2b398250b 100644
--- a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidgetFactoryTest.kt
+++ b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidgetFactoryTest.kt
@@ -21,9 +21,7 @@ import app.cash.redwood.protocol.Event
 import app.cash.redwood.protocol.EventSink
 import app.cash.redwood.protocol.Id
 import app.cash.redwood.protocol.PropertyDiff
-import example.redwood.compose.accessibilityDescription
-import example.redwood.compose.customType
-import example.redwood.compose.customTypeWithDefault
+import example.redwood.compose.TestScope
 import example.redwood.widget.Button
 import example.redwood.widget.DiffConsumingExampleSchemaWidgetFactory
 import example.redwood.widget.ExampleSchemaWidgetFactory
@@ -98,7 +96,9 @@ class DiffConsumingWidgetFactoryTest {
       },
     )
 
-    assertEquals(LayoutModifier.customType(10.seconds), textInput.layoutModifiers)
+    with(object : TestScope {}) {
+      assertEquals(LayoutModifier.customType(10.seconds), textInput.layoutModifiers)
+    }
   }
 
   @Test fun layoutModifierDeserializationHonorsDefaultExpressions() {
@@ -131,7 +131,9 @@ class DiffConsumingWidgetFactoryTest {
       },
     )
 
-    assertEquals(LayoutModifier.customTypeWithDefault(10.seconds, "sup"), textInput.layoutModifiers)
+    with(object : TestScope {}) {
+      assertEquals(LayoutModifier.customTypeWithDefault(10.seconds, "sup"), textInput.layoutModifiers)
+    }
   }
 
   @Test fun unknownLayoutModifierThrowsDefault() {
@@ -190,7 +192,9 @@ class DiffConsumingWidgetFactoryTest {
     assertEquals("Unknown layout modifier 345432", handler.events.single())
 
     // Ensure only the invalid LayoutModifier was discarded and not all of them.
-    assertEquals(LayoutModifier.accessibilityDescription("hi"), recordingTextInput.layoutModifiers)
+    with(object : TestScope {}) {
+      assertEquals(LayoutModifier.accessibilityDescription("hi"), recordingTextInput.layoutModifiers)
+    }
   }
 
   @Test fun unknownChildrenThrowsDefault() {
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 250d1bc6f..cd067d54b 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
@@ -27,6 +27,8 @@ import kotlin.reflect.full.createType
 import org.junit.Test
 
 class SchemaParserTest {
+  object TestScope
+
   interface NonAnnotationSchema
 
   @Test fun nonAnnotatedSchemaThrows() {
@@ -63,7 +65,7 @@ class SchemaParserTest {
   interface DoubleAnnotatedWidgetSchema
 
   @Widget(1)
-  @LayoutModifier(1)
+  @LayoutModifier(1, TestScope::class)
   data class DoubleAnnotatedWidget(
     @Property(1) val name: String,
   )
@@ -121,17 +123,17 @@ class SchemaParserTest {
   )
   interface DuplicateLayoutModifierTagSchema
 
-  @LayoutModifier(1)
+  @LayoutModifier(1, TestScope::class)
   data class DuplicateLayoutModifierTagA(
     val name: String,
   )
 
-  @LayoutModifier(2)
+  @LayoutModifier(2, TestScope::class)
   data class NonDuplicateLayoutModifierTag(
     val name: String,
   )
 
-  @LayoutModifier(1)
+  @LayoutModifier(1, TestScope::class)
   data class DuplicateLayoutModifierTagB(
     val name: String,
   )
@@ -274,7 +276,7 @@ class SchemaParserTest {
   )
   interface NonDataClassLayoutModifierSchema
 
-  @LayoutModifier(1)
+  @LayoutModifier(1, TestScope::class)
   class NonDataClassLayoutModifier(
     @Property(1) val name: String,
   )
@@ -519,7 +521,7 @@ class SchemaParserTest {
   )
   interface OneMillionLayoutModifierSchema
 
-  @LayoutModifier(1_000_000)
+  @LayoutModifier(1_000_000, TestScope::class)
   data class OneMillionLayoutModifier(
     val value: Int,
   )
@@ -540,7 +542,7 @@ class SchemaParserTest {
   )
   interface ZeroLayoutModifierSchema
 
-  @LayoutModifier(0)
+  @LayoutModifier(0, TestScope::class)
   data class ZeroLayoutModifier(
     val value: Int,
   )
@@ -563,7 +565,7 @@ class SchemaParserTest {
     @Children(1) val children: () -> Unit,
   )
 
-  @LayoutModifier(1)
+  @LayoutModifier(1, TestScope::class)
   data class SomeLayoutModifier(
     val value: Int,
   )
@@ -767,4 +769,23 @@ class SchemaParserTest {
       """.trimMargin(),
     )
   }
+
+  @Schema(
+    members = [
+      UnscopedLayoutModifier::class,
+    ],
+  )
+  interface UnscopedModifierSchema
+
+  @LayoutModifier(1)
+  object UnscopedLayoutModifier
+
+  @Test fun `layout modifier must have at least one scope`() {
+    assertThrows<IllegalArgumentException> {
+      parseSchema(UnscopedModifierSchema::class)
+    }.hasMessageThat().isEqualTo(
+      "@LayoutModifier app.cash.redwood.schema.parser.SchemaParserTest.UnscopedLayoutModifier " +
+        "must have at least one scope.",
+    )
+  }
 }
diff --git a/test-schema/src/main/kotlin/example/redwood/schema.kt b/test-schema/src/main/kotlin/example/redwood/schema.kt
index 58a6fc43c..8248024cb 100644
--- a/test-schema/src/main/kotlin/example/redwood/schema.kt
+++ b/test-schema/src/main/kotlin/example/redwood/schema.kt
@@ -80,33 +80,34 @@ public data class TextInput(
 @Widget(6)
 public object Space
 
+public object TestScope
+
+public object SecondaryTestScope
+
 @LayoutModifier(1, RowScope::class)
 public data class RowVerticalAlignment(
   /** -1 for top, 0 for middle, 1 for bottom. */
   val direction: Int,
 )
 
-@LayoutModifier(2)
+@LayoutModifier(2, TestScope::class)
 public data class AccessibilityDescription(
   val value: String,
 )
 
-@LayoutModifier(3)
+@LayoutModifier(3, TestScope::class)
 public data class CustomType(
   val customType: Duration,
 )
 
-@LayoutModifier(4)
+@LayoutModifier(4, TestScope::class)
 public object CustomTypeStateless
 
-@LayoutModifier(5)
+@LayoutModifier(5, TestScope::class)
 public data class CustomTypeWithDefault(
   val customType: Duration,
   @Default("\"sup\"") val string: String,
 )
 
-public object TestScopeOne
-public object TestScopeTwo
-
-@LayoutModifier(6, scopes = [TestScopeOne::class, TestScopeTwo::class])
+@LayoutModifier(6, TestScope::class, SecondaryTestScope::class)
 public object CustomTypeWithMultipleScopes