Repository: eiriktsarpalis/PolyType
Pull request #126: Add `IUnionCaseShape.IsTagSpecified` property
Author: login: AArnott
Created: 2025-03-04T18:02:25Z | Merged: 2025-03-04T19:21:16Z

This allows a serializer to know whether they can consider the Tag property to be a 'stable' input into the serialized schema.


Diff:
diff --git a/src/PolyType.Roslyn/Model/DerivedTypeModel.cs b/src/PolyType.Roslyn/Model/DerivedTypeModel.cs
index 19f01290..4649f5f3 100644
--- a/src/PolyType.Roslyn/Model/DerivedTypeModel.cs
+++ b/src/PolyType.Roslyn/Model/DerivedTypeModel.cs
@@ -22,6 +22,11 @@ public sealed class DerivedTypeModel
     /// </summary>
     public required int Tag { get; init; }
 
+    /// <summary>
+    /// Gets a value indicating whether <see cref="Tag"/> has been explicitly specified or inferred in a less stable way.
+    /// </summary>
+    public required bool IsTagSpecified { get; init; }
+
     /// <summary>
     /// The index of the derived type.
     /// </summary>
diff --git a/src/PolyType.Roslyn/ModelGenerator/TypeDataModelGenerator.cs b/src/PolyType.Roslyn/ModelGenerator/TypeDataModelGenerator.cs
index c208cb58..ff42576f 100644
--- a/src/PolyType.Roslyn/ModelGenerator/TypeDataModelGenerator.cs
+++ b/src/PolyType.Roslyn/ModelGenerator/TypeDataModelGenerator.cs
@@ -302,7 +302,7 @@ private ImmutableArray<DerivedTypeModel> IncludeDerivedTypes(ITypeSymbol type, r
     {
         // 1. Resolve the shapes for all derived types.
         List<DerivedTypeModel> derivedTypeModels = [];
-        DerivedTypeModel baseTypeModel = new() { Type = type, Name = null!, Tag = -1, Index = -1, IsBaseType = true };
+        DerivedTypeModel baseTypeModel = new() { Type = type, Name = null!, Tag = -1, IsTagSpecified = false, Index = -1, IsBaseType = true };
         foreach (DerivedTypeModel derivedType in ResolveDerivedTypes(type))
         {
             if (IncludeNestedType(derivedType.Type, ref ctx) is TypeDataModelGenerationStatus.Success)
diff --git a/src/PolyType.SourceGenerator/Model/UnionShapeModel.cs b/src/PolyType.SourceGenerator/Model/UnionShapeModel.cs
index 319d3057..728d1b30 100644
--- a/src/PolyType.SourceGenerator/Model/UnionShapeModel.cs
+++ b/src/PolyType.SourceGenerator/Model/UnionShapeModel.cs
@@ -17,6 +17,7 @@ public sealed record UnionCaseModel
     public required TypeId Type { get; init; }
     public required string Name { get; init; }
     public required int Tag { get; init; }
+    public required bool IsTagSpecified { get; init; }
     public required int Index { get; init; }
     public required bool IsBaseType { get; init; }
 }
diff --git a/src/PolyType.SourceGenerator/Parser/Parser.ModelMapper.cs b/src/PolyType.SourceGenerator/Parser/Parser.ModelMapper.cs
index d7ff2821..f2a3e095 100644
--- a/src/PolyType.SourceGenerator/Parser/Parser.ModelMapper.cs
+++ b/src/PolyType.SourceGenerator/Parser/Parser.ModelMapper.cs
@@ -222,6 +222,7 @@ private static UnionShapeModel MapUnionModel(TypeDataModel model, TypeShapeModel
                     Type = CreateTypeId(derived.Type),
                     Name = derived.Name,
                     Tag = derived.Tag,
+                    IsTagSpecified = derived.IsTagSpecified,
                     Index = derived.Index,
                     IsBaseType = derived.IsBaseType,
                 })
diff --git a/src/PolyType.SourceGenerator/Parser/Parser.cs b/src/PolyType.SourceGenerator/Parser/Parser.cs
index d1129387..337c75e9 100644
--- a/src/PolyType.SourceGenerator/Parser/Parser.cs
+++ b/src/PolyType.SourceGenerator/Parser/Parser.cs
@@ -162,7 +162,8 @@ protected override IEnumerable<DerivedTypeModel> ResolveDerivedTypes(ITypeSymbol
                 continue;
             }
 
-            tag = tag < 0 ? i : tag;
+            bool isTagSpecified = tag >= 0;
+            tag = isTagSpecified ? tag : i;
             name ??= derivedType.Name;
 
             if (!types.Add(derivedType))
@@ -188,6 +189,7 @@ protected override IEnumerable<DerivedTypeModel> ResolveDerivedTypes(ITypeSymbol
                 Type = derivedType,
                 Name = name,
                 Tag = tag,
+                IsTagSpecified = isTagSpecified,
                 Index = i,
                 IsBaseType = SymbolEqualityComparer.Default.Equals(derivedType, type),
             };
diff --git a/src/PolyType.SourceGenerator/SourceFormatter/SourceFormatter.FSharpUnion.cs b/src/PolyType.SourceGenerator/SourceFormatter/SourceFormatter.FSharpUnion.cs
index f1af0ce2..8ab82982 100644
--- a/src/PolyType.SourceGenerator/SourceFormatter/SourceFormatter.FSharpUnion.cs
+++ b/src/PolyType.SourceGenerator/SourceFormatter/SourceFormatter.FSharpUnion.cs
@@ -46,6 +46,7 @@ private static void FormatFSharpUnionCasesFactory(SourceWriter writer, FSharpUni
                     Type = {{unionCase.TypeModel.SourceIdentifier}},
                     Name = "{{unionCase.Name}}",
                     Tag = {{unionCase.Tag}},
+                    IsTagSpecified = false,
                     Index = {{unionCase.Tag}},
                 },
                 """);
diff --git a/src/PolyType.SourceGenerator/SourceFormatter/SourceFormatter.Union.cs b/src/PolyType.SourceGenerator/SourceFormatter/SourceFormatter.Union.cs
index dd122439..825784c8 100644
--- a/src/PolyType.SourceGenerator/SourceFormatter/SourceFormatter.Union.cs
+++ b/src/PolyType.SourceGenerator/SourceFormatter/SourceFormatter.Union.cs
@@ -57,8 +57,9 @@ private void FormatUnionCasesFactory(SourceWriter writer, UnionShapeModel unionS
                 new global::PolyType.SourceGenModel.SourceGenUnionCaseShape<{{unionCase.Type.FullyQualifiedName}}, {{unionShapeModel.Type.FullyQualifiedName}}>
                 {
                     Type = {{unionCaseType.SourceIdentifier}},
-                    Name = "{{unionCase.Name}}",
+                    Name = {{FormatStringLiteral(unionCase.Name)}},
                     Tag = {{unionCase.Tag}},
+                    IsTagSpecified = {{FormatBool(unionCase.IsTagSpecified)}},
                     Index = {{unionCase.Index}},
                 },
                 """);
diff --git a/src/PolyType/Abstractions/IUnionCaseShape.cs b/src/PolyType/Abstractions/IUnionCaseShape.cs
index a7d881ff..cc59a469 100644
--- a/src/PolyType/Abstractions/IUnionCaseShape.cs
+++ b/src/PolyType/Abstractions/IUnionCaseShape.cs
@@ -23,6 +23,11 @@ public interface IUnionCaseShape
     /// </remarks>
     int Tag { get; }
 
+    /// <summary>
+    /// Gets a value indicating whether <see cref="Tag"/> has been explicitly specified or inferred in a less stable way.
+    /// </summary>
+    bool IsTagSpecified { get; }
+
     /// <summary>
     /// Gets the unique index corresponding to the current union case.
     /// </summary>
diff --git a/src/PolyType/ReflectionProvider/FSharpUnionTypeShape.cs b/src/PolyType/ReflectionProvider/FSharpUnionTypeShape.cs
index fc87ae3b..78446864 100644
--- a/src/PolyType/ReflectionProvider/FSharpUnionTypeShape.cs
+++ b/src/PolyType/ReflectionProvider/FSharpUnionTypeShape.cs
@@ -58,6 +58,7 @@ internal sealed class FSharpUnionCaseShape<TUnionCase, TUnion>(FSharpUnionCaseIn
     public ITypeShape<TUnionCase> Type { get; } = new FSharpUnionCaseTypeShape<TUnionCase>(unionCaseInfo, provider);
     public string Name => unionCaseInfo.Name;
     public int Tag => unionCaseInfo.Tag;
+    public bool IsTagSpecified => false; // F# tags are inferred from the union case ordering
     public int Index => unionCaseInfo.Tag;
     ITypeShape IUnionCaseShape.Type => Type;
     public object? Accept(ITypeShapeVisitor visitor, object? state = null) => visitor.VisitUnionCase(this, state);
diff --git a/src/PolyType/ReflectionProvider/ReflectionTypeShapeProvider.cs b/src/PolyType/ReflectionProvider/ReflectionTypeShapeProvider.cs
index 1a3a88f8..18a086a0 100644
--- a/src/PolyType/ReflectionProvider/ReflectionTypeShapeProvider.cs
+++ b/src/PolyType/ReflectionProvider/ReflectionTypeShapeProvider.cs
@@ -422,9 +422,10 @@ internal IConstructorParameterShape CreateConstructorParameter(Type constructorA
     internal IUnionCaseShape CreateUnionCaseShape(IUnionTypeShape unionTypeShape, DerivedTypeShapeAttribute derivedTypeAttribute, int index)
     {
         string name = derivedTypeAttribute.Name;
-        int tag = derivedTypeAttribute.Tag < 0 ? index : derivedTypeAttribute.Tag;
+        bool isTagSpecified = derivedTypeAttribute.Tag >= 0;
+        int tag = isTagSpecified ? derivedTypeAttribute.Tag : index;
         Type unionCaseType = typeof(ReflectionUnionCaseShape<,>).MakeGenericType(derivedTypeAttribute.Type, unionTypeShape.Type);
-        return (IUnionCaseShape)Activator.CreateInstance(unionCaseType, unionTypeShape, name, tag, index, this)!;
+        return (IUnionCaseShape)Activator.CreateInstance(unionCaseType, unionTypeShape, name, tag, isTagSpecified, index, this)!;
     }
 
     internal static IConstructorShapeInfo CreateTupleConstructorShapeInfo(Type tupleType)
diff --git a/src/PolyType/ReflectionProvider/ReflectionUnionCaseShape.cs b/src/PolyType/ReflectionProvider/ReflectionUnionCaseShape.cs
index fc2eb636..bdb629cb 100644
--- a/src/PolyType/ReflectionProvider/ReflectionUnionCaseShape.cs
+++ b/src/PolyType/ReflectionProvider/ReflectionUnionCaseShape.cs
@@ -2,7 +2,7 @@
 
 namespace PolyType.ReflectionProvider;
 
-internal sealed class ReflectionUnionCaseShape<TUnionCase, TUnion>(IUnionTypeShape unionType, string name, int tag, int index, ReflectionTypeShapeProvider provider) : IUnionCaseShape<TUnionCase, TUnion>
+internal sealed class ReflectionUnionCaseShape<TUnionCase, TUnion>(IUnionTypeShape unionType, string name, int tag, bool isTagSpecified, int index, ReflectionTypeShapeProvider provider) : IUnionCaseShape<TUnionCase, TUnion>
     where TUnionCase : TUnion
 {
     public ITypeShape<TUnionCase> Type => _type ??= typeof(TUnionCase) == typeof(TUnion) ? (ITypeShape<TUnionCase>)unionType.BaseType : provider.GetShape<TUnionCase>();
@@ -10,6 +10,7 @@ internal sealed class ReflectionUnionCaseShape<TUnionCase, TUnion>(IUnionTypeSha
 
     public string Name { get; } = name;
     public int Tag { get; } = tag;
+    public bool IsTagSpecified { get; } = isTagSpecified;
     public int Index { get; } = index;
 
     ITypeShape IUnionCaseShape.Type => Type;
diff --git a/src/PolyType/SourceGenModel/SourceGenUnionCaseShape.cs b/src/PolyType/SourceGenModel/SourceGenUnionCaseShape.cs
index 357b54fe..a2bb877c 100644
--- a/src/PolyType/SourceGenModel/SourceGenUnionCaseShape.cs
+++ b/src/PolyType/SourceGenModel/SourceGenUnionCaseShape.cs
@@ -25,6 +25,9 @@ public sealed class SourceGenUnionCaseShape<TUnionCase, TUnion> : IUnionCaseShap
     /// </summary>
     public required int Tag { get; init; }
 
+    /// <inheritdoc/>
+    public required bool IsTagSpecified { get; init; }
+
     /// <summary>
     /// Gets the unique index corresponding to the current union case.
     /// </summary>
diff --git a/tests/PolyType.Tests/TypeShapeProviderTests.cs b/tests/PolyType.Tests/TypeShapeProviderTests.cs
index ebebe92d..365ff42f 100644
--- a/tests/PolyType.Tests/TypeShapeProviderTests.cs
+++ b/tests/PolyType.Tests/TypeShapeProviderTests.cs
@@ -18,12 +18,12 @@ public abstract class TypeShapeProviderTests(ProviderUnderTest providerUnderTest
     public void TypeShapeReportsExpectedInfo<T>(TestCase<T> testCase)
     {
         ITypeShape<T> shape = providerUnderTest.ResolveShape(testCase);
-        
+
         Assert.Equal(typeof(T), shape.Type);
         Assert.Equal(typeof(T), shape.AttributeProvider);
-        Assert.Equal(typeof(T).IsRecordType() && testCase is { UsesMarshaller: false, IsUnion: false }, shape is IObjectTypeShape { IsRecordType: true});
+        Assert.Equal(typeof(T).IsRecordType() && testCase is { UsesMarshaller: false, IsUnion: false }, shape is IObjectTypeShape { IsRecordType: true });
         Assert.Equal(typeof(T).IsTupleType() && testCase is { UsesMarshaller: false, IsUnion: false }, shape is IObjectTypeShape { IsTupleType: true });
-        
+
         Assert.Same(providerUnderTest.Provider, shape.Provider);
         Assert.Same(shape, shape.Provider.GetShape(shape.Type));
 
@@ -135,7 +135,7 @@ public void GetConstructors<T>(TestCase<T> testCase)
         {
             return;
         }
-        
+
         var visitor = new ConstructorTestVisitor();
         if (objectShape.Constructor is { } ctor)
         {
@@ -155,7 +155,7 @@ private sealed class ConstructorTestVisitor : TypeShapeVisitor
             {
                 Assert.Throws<InvalidOperationException>(() => constructor.GetArgumentStateConstructor());
                 Assert.Throws<InvalidOperationException>(() => constructor.GetParameterizedConstructor());
-                
+
                 var defaultCtor = constructor.GetDefaultConstructor();
                 Assert.Same(constructor.GetDefaultConstructor(), constructor.GetDefaultConstructor());
                 TDeclaringType defaultValue = defaultCtor();
@@ -164,7 +164,7 @@ private sealed class ConstructorTestVisitor : TypeShapeVisitor
             else
             {
                 Assert.Throws<InvalidOperationException>(() => constructor.GetDefaultConstructor());
-                
+
                 int i = 0;
                 var argumentStateCtor = constructor.GetArgumentStateConstructor();
                 Assert.NotNull(argumentStateCtor);
@@ -187,7 +187,7 @@ private sealed class ConstructorTestVisitor : TypeShapeVisitor
                     Assert.NotNull(value);
                 }
             }
-            
+
             return null;
         }
 
@@ -263,7 +263,7 @@ private sealed class OptionalTestVisitor : TypeShapeVisitor
             return null;
         }
     }
-    
+
     [Theory]
     [MemberData(nameof(TestTypes.GetTestCases), MemberType = typeof(TestTypes))]
     public void GetSurrogateType<T>(TestCase<T> testCase)
@@ -292,7 +292,7 @@ private sealed class SurrogateTestVisitor : TypeShapeVisitor
             {
                 marshallerType = marshallerType.MakeGenericType(typeof(T).GetGenericArguments());
             }
-            
+
             Assert.Equal(typeof(T), surrogateShape.Type);
             Assert.Equal(typeof(TSurrogate), surrogateShape.SurrogateType.Type);
             Assert.IsType(marshallerType, surrogateShape.Marshaller);
@@ -309,6 +309,7 @@ public void GetUnionType<T>(TestCase<T> testCase)
         if (shape.Kind is TypeShapeKind.Union)
         {
             IUnionTypeShape<T> unionShape = Assert.IsAssignableFrom<IUnionTypeShape<T>>(shape);
+            DerivedTypeShapeAttribute[] attributes = unionShape.AttributeProvider?.GetCustomAttributes(typeof(DerivedTypeShapeAttribute), false)?.Cast<DerivedTypeShapeAttribute>().ToArray() ?? [];
             Assert.NotSame(shape, unionShape.BaseType);
             Assert.NotEmpty(unionShape.UnionCases);
             int i = 0;
@@ -317,6 +318,9 @@ public void GetUnionType<T>(TestCase<T> testCase)
                 Assert.True(typeof(T).IsAssignableFrom(unionCase.Type.Type));
                 Assert.NotNull(unionCase.Name);
                 Assert.Equal(i++, unionCase.Index);
+
+                DerivedTypeShapeAttribute? attribute = attributes.FirstOrDefault(a => a.Type == unionCase.Type.Type);
+                Assert.Equal(unionCase.IsTagSpecified, attribute is not null && attribute.Tag != -1);
             }
 
             Getter<T, int> unionCaseIndexGetter = unionShape.GetGetUnionCaseIndex();
@@ -764,13 +768,13 @@ public void ReflectionTypeShapeProvider_Default_UsesReflectionEmit()
     {
         Assert.True(ReflectionTypeShapeProvider.Default.Options.UseReflectionEmit);
     }
-    
+
     [Fact]
     public void ReflectionTypeShapeProvider_Default_IsSingleton()
     {
         Assert.Same(ReflectionTypeShapeProvider.Default, ReflectionTypeShapeProvider.Default);
     }
-    
+
     [Theory]
     [InlineData(false)]
     [InlineData(true)]
@@ -780,7 +784,7 @@ public void ReflectionTypeShapeProvider_Default_Factory_ReflectsParameters(bool
         ReflectionTypeShapeProvider provider = ReflectionTypeShapeProvider.Create(options);
         Assert.Equal(useReflectionEmit, provider.Options.UseReflectionEmit);
     }
-    
+
     [Theory]
     [InlineData(typeof(ClassWithEnumKind))]
     [InlineData(typeof(ClassWithNullableKind))]
@@ -832,7 +836,7 @@ public void ClassWithInvalidMarshallers_ThrowsInvalidOperationException(Type typ
 
     [TypeShape(Marshaller = typeof(int))]
     private class ClassWithInvalidMarshaller;
-    
+
     [TypeShape(Marshaller = typeof(Marshaller))]
     private class ClassWithMismatchingMarshaller
     {


Reviews:
user: login: eiriktsarpalis
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-04T18:08:42Z
user: login: eiriktsarpalis
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-04T18:12:32Z
user: login: eiriktsarpalis
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-04T18:13:18Z
user: login: eiriktsarpalis
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-04T18:15:30Z
user: login: eiriktsarpalis
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-04T18:16:48Z
user: login: eiriktsarpalis
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-04T18:17:24Z
user: login: eiriktsarpalis
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-04T18:19:18Z
user: login: eiriktsarpalis
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-04T18:36:14Z

Review comments:
diff_hunk: @@ -22,6 +22,11 @@ public sealed class DerivedTypeModel
     /// </summary>
     public required int Tag { get; init; }
 
+    /// <summary>
+    /// Gets a value indicating whether <see cref="Tag"/> is inferred or explicitly specified in a more stable way.
path: src/PolyType.Roslyn/Model/DerivedTypeModel.cs
user: login: eiriktsarpalis
body: I would invert the order to emphasize the meaning of `true`.
```suggestion
    /// Gets a value indicating whether <see cref="Tag"/> has been explicitly specified or inferred in a less stable way.
```
created_at: 2025-03-04T18:08:42Z
author_association: OWNER
diff_hunk: @@ -59,6 +59,7 @@ private void FormatUnionCasesFactory(SourceWriter writer, UnionShapeModel unionS
                     Type = {{unionCaseType.SourceIdentifier}},
                     Name = "{{unionCase.Name}}",
                     Tag = {{unionCase.Tag}},
+                    IsTagSpecified = {{unionCase.IsTagSpecified.ToString().ToLowerInvariant()}},
path: src/PolyType.SourceGenerator/SourceFormatter/SourceFormatter.Union.cs
user: login: eiriktsarpalis
body: ```suggestion
                    IsTagSpecified = {{FormatBool(unionCase.IsTagSpecified)}},
```
created_at: 2025-03-04T18:12:32Z
author_association: OWNER
diff_hunk: @@ -59,6 +59,7 @@ private void FormatUnionCasesFactory(SourceWriter writer, UnionShapeModel unionS
                     Type = {{unionCaseType.SourceIdentifier}},
                     Name = "{{unionCase.Name}}",
path: src/PolyType.SourceGenerator/SourceFormatter/SourceFormatter.Union.cs
user: login: eiriktsarpalis
body: Clear case of a Copilot-induced bug:
```suggestion
                    Name = {{FormatStringLiteral(unionCase.Name)}},
```
created_at: 2025-03-04T18:13:18Z
author_association: OWNER
diff_hunk: @@ -46,6 +46,7 @@ private static void FormatFSharpUnionCasesFactory(SourceWriter writer, FSharpUni
                     Type = {{unionCase.TypeModel.SourceIdentifier}},
                     Name = "{{unionCase.Name}}",
                     Tag = {{unionCase.Tag}},
+                    IsTagSpecified = true,
path: src/PolyType.SourceGenerator/SourceFormatter/SourceFormatter.FSharpUnion.cs
user: login: eiriktsarpalis
body: The tag in F# unions just follows the order of union case declarations, so probably not something we can consider stable.
```suggestion
                    IsTagSpecified = false,
```
created_at: 2025-03-04T18:15:30Z
author_association: OWNER
diff_hunk: @@ -23,6 +23,11 @@ public interface IUnionCaseShape
     /// </remarks>
     int Tag { get; }
 
+    /// <summary>
+    /// Gets a value indicating whether <see cref="Tag"/> is inferred or explicitly specified in a more stable way.
path: src/PolyType/Abstractions/IUnionCaseShape.cs
user: login: eiriktsarpalis
body: Could you update the doc per https://github.com/eiriktsarpalis/PolyType/pull/126/files#r1979964214?
created_at: 2025-03-04T18:16:48Z
author_association: OWNER
diff_hunk: @@ -58,6 +58,7 @@ internal sealed class FSharpUnionCaseShape<TUnionCase, TUnion>(FSharpUnionCaseIn
     public ITypeShape<TUnionCase> Type { get; } = new FSharpUnionCaseTypeShape<TUnionCase>(unionCaseInfo, provider);
     public string Name => unionCaseInfo.Name;
     public int Tag => unionCaseInfo.Tag;
+    public bool IsTagSpecified => true; // F# considers all unions to have stable tags
path: src/PolyType/ReflectionProvider/FSharpUnionTypeShape.cs
user: login: eiriktsarpalis
body: ```suggestion
    public bool IsTagSpecified => false; // F# tags are inferred from the union case ordering
```
created_at: 2025-03-04T18:17:24Z
author_association: OWNER
diff_hunk: @@ -362,6 +362,13 @@ public void GetDictionaryType<T>(TestCase<T> testCase)
         }
     }
 
+    [Fact]
+    public void IsTagSpecified()
+    {
+        Assert.True(((IUnionTypeShape?)Provider.GetShape(typeof(Tree)))?.UnionCases[0].IsTagSpecified);
path: tests/PolyType.Tests/TypeShapeProviderTests.cs
user: login: eiriktsarpalis
body: Could you convert this to a theory that validates the value of the property based on the result from querying the relevant attribute?
created_at: 2025-03-04T18:19:18Z
author_association: OWNER
diff_hunk: @@ -362,6 +362,13 @@ public void GetDictionaryType<T>(TestCase<T> testCase)
         }
     }
 
+    [Fact]
+    public void IsTagSpecified()
+    {
+        Assert.True(((IUnionTypeShape?)Provider.GetShape(typeof(Tree)))?.UnionCases[0].IsTagSpecified);
path: tests/PolyType.Tests/TypeShapeProviderTests.cs
user: login: eiriktsarpalis
body: Actually, there's a theory on this class already that specifically checks `IUnionTypeShape` instances. Incorporating it there could work as well.
created_at: 2025-03-04T18:36:14Z
author_association: OWNER

PR comments:
user: login: codecov[bot]
created_at: 2025-03-04T18:08:50Z
body: ## [Codecov](https://app.codecov.io/gh/eiriktsarpalis/PolyType/pull/126?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Eirik+Tsarpalis) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 94.45%. Comparing base [(`2b8ae6c`)](https://app.codecov.io/gh/eiriktsarpalis/PolyType/commit/2b8ae6c46d17d82294c6e2da39a3115e88a6c0c6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Eirik+Tsarpalis) to head [(`c5e8811`)](https://app.codecov.io/gh/eiriktsarpalis/PolyType/commit/c5e8811666a8188acf05bce83ca22a20ee2fbc14?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Eirik+Tsarpalis).
> Report is 1 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #126      +/-   ##
==========================================
+ Coverage   94.43%   94.45%   +0.01%     
==========================================
  Files         220      220              
  Lines       13936    13950      +14     
  Branches     1847     1841       -6     
==========================================
+ Hits        13161    13176      +15     
  Misses        742      742              
+ Partials       33       32       -1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/eiriktsarpalis/PolyType/pull/126?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Eirik+Tsarpalis).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Eirik+Tsarpalis).

author_association: NONE
user: login: AArnott
created_at: 2025-03-04T19:09:08Z
body: Feedback addressed.
author_association: CONTRIBUTOR