Skip to content

Commit c5ed2c8

Browse files
authored
fix: error on duplicate serialized types (#180)
1 parent 742e258 commit c5ed2c8

File tree

5 files changed

+82
-22
lines changed

5 files changed

+82
-22
lines changed

src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,53 @@ public void WhenNoSerializableTypesIsEmpty ()
2323
}
2424

2525
[Fact]
26-
public void DoesntSerializeInstancedInteropInterfaces ()
26+
public void SerializesTypesFromInteropMethods ()
27+
{
28+
AddAssembly(With(
29+
"""
30+
public record RecordA;
31+
public record RecordB;
32+
public record RecordC;
33+
34+
public class Class
35+
{
36+
[JSInvokable] public static Task<RecordA[]> A (RecordC c) => default;
37+
[JSFunction] public static RecordB[] B (RecordC[] c) => default;
38+
}
39+
"""));
40+
Execute();
41+
Contains("[JsonSerializable(typeof(global::RecordA)");
42+
Contains("[JsonSerializable(typeof(global::RecordB)");
43+
Contains("[JsonSerializable(typeof(global::RecordC)");
44+
Contains("[JsonSerializable(typeof(global::RecordA[])");
45+
Contains("[JsonSerializable(typeof(global::RecordB[])");
46+
Contains("[JsonSerializable(typeof(global::RecordC[])");
47+
}
48+
49+
[Fact]
50+
public void SerializesTypesFromInteropInterfaces ()
51+
{
52+
AddAssembly(With(
53+
"""
54+
public record RecordA;
55+
public record RecordB;
56+
public record RecordC;
57+
public interface IExported { void Inv (RecordA a); }
58+
public interface IImported { void Fun (RecordB b); void NotifyEvt(RecordC c); }
59+
60+
public class Class
61+
{
62+
[JSFunction] public static Task<IImported> GetImported (IExported arg) => default;
63+
}
64+
"""));
65+
Execute();
66+
Contains("[JsonSerializable(typeof(global::RecordA)");
67+
Contains("[JsonSerializable(typeof(global::RecordB)");
68+
Contains("[JsonSerializable(typeof(global::RecordC)");
69+
}
70+
71+
[Fact]
72+
public void DoesntSerializeInstancedInteropInterfacesThemselves ()
2773
{
2874
AddAssembly(With(
2975
"""
@@ -46,23 +92,32 @@ public class Class
4692
DoesNotContain("JsonSerializable");
4793
}
4894

49-
[Fact] // .NET's generator indexes types by short names (w/o namespace) and fails on duplicates.
50-
public void AddsOnlyTopLevelTypesAndCrawledDuplicates ()
95+
[Fact]
96+
public void SerializesAllTheCrawledSerializableTypes ()
5197
{
98+
// .NET's generator indexes types by short names (w/o namespace) and fails on duplicates, so we have to add everything ourselves.
99+
// https://github.com/dotnet/runtime/issues/58938#issuecomment-1306731801
52100
AddAssembly(
53-
With("y", "public struct Struct { public double A { get; set; } }"),
54-
With("n", "public struct Struct { public y.Struct S { get; set; } }"),
101+
With("y", "public enum Enum { A, B }"),
102+
With("y", "public record Struct (double A, ReadonlyStruct[]? B);"),
103+
With("y", "public record ReadonlyStruct (Enum e);"),
104+
With("n", "public struct Struct { public y.Struct S { get; set; } public ReadonlyStruct[]? A { get; set; } }"),
55105
With("n", "public readonly struct ReadonlyStruct { public double A { get; init; } }"),
56106
With("n", "public readonly record struct ReadonlyRecordStruct(double A);"),
57107
With("n", "public record class RecordClass(double A);"),
58108
With("n", "public enum Enum { A, B }"),
59109
With("n", "public class Foo { public Struct S { get; } public ReadonlyStruct Rs { get; } }"),
60110
WithClass("n", "public class Bar : Foo { public ReadonlyRecordStruct Rrs { get; } public RecordClass Rc { get; } }"),
61-
With("n", "public class Baz { public List<Class.Bar?> Bars { get; } public Enum E { get; } }"),
62-
WithClass("n", "[JSInvokable] public static Task<Baz?> GetBaz () => default;"));
111+
With("n", "public class Baz { public List<Class.Bar?> Bars { get; } }"),
112+
WithClass("n", "[JSInvokable] public static Task<Baz?> GetBaz (Enum e) => default;"));
63113
Execute();
64-
Assert.Equal(2, Matches("JsonSerializable").Count);
65-
Contains("[JsonSerializable(typeof(global::n.Baz)");
114+
Contains("[JsonSerializable(typeof(global::y.Enum)");
115+
Contains("[JsonSerializable(typeof(global::n.Enum)");
66116
Contains("[JsonSerializable(typeof(global::y.Struct)");
117+
Contains("[JsonSerializable(typeof(global::n.Struct)");
118+
Contains("[JsonSerializable(typeof(global::n.ReadonlyStruct)");
119+
Contains("[JsonSerializable(typeof(global::y.ReadonlyStruct)");
120+
Contains("[JsonSerializable(typeof(global::n.ReadonlyStruct[])");
121+
Contains("[JsonSerializable(typeof(global::y.ReadonlyStruct[])");
67122
}
68123
}

src/cs/Bootsharp.Publish/Common/TypeConverter/TypeCrawler.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ internal sealed class TypeCrawler
99
public void Crawl (Type type)
1010
{
1111
if (!ShouldCrawl(type)) return;
12-
type = GetUnderlyingType(type);
13-
if (!crawled.Add(type)) return;
14-
CrawlProperties(type);
15-
CrawlBaseType(type);
12+
var underlyingType = GetUnderlyingType(type);
13+
if (!crawled.Add(underlyingType)) return;
14+
CrawlProperties(underlyingType);
15+
CrawlBaseType(underlyingType);
16+
crawled.Add(type);
1617
}
1718

1819
private bool ShouldCrawl (Type type)

src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ internal sealed class SerializerGenerator
1111

1212
public string Generate (SolutionInspection inspection)
1313
{
14-
CollectAttributes(inspection);
15-
CollectDuplicates(inspection);
14+
CollectTopLevel(inspection);
15+
CollectCrawled(inspection);
1616
if (attributes.Count == 0) return "";
1717
return
1818
$"""
@@ -30,7 +30,7 @@ internal partial class SerializerContext : JsonSerializerContext;
3030
""";
3131
}
3232

33-
private void CollectAttributes (SolutionInspection inspection)
33+
private void CollectTopLevel (SolutionInspection inspection)
3434
{
3535
var metas = inspection.StaticMethods
3636
.Concat(inspection.StaticInterfaces.SelectMany(i => i.Methods))
@@ -53,11 +53,10 @@ private void CollectFromValue (ValueMeta meta)
5353
attributes.Add(BuildAttribute(meta.Type));
5454
}
5555

56-
private void CollectDuplicates (SolutionInspection inspection)
56+
private void CollectCrawled (SolutionInspection inspection)
5757
{
58-
var names = new HashSet<string>();
59-
foreach (var type in inspection.Crawled.DistinctBy(t => t.FullName))
60-
if (ShouldSerialize(type) && !names.Add(type.Name))
58+
foreach (var type in inspection.Crawled)
59+
if (ShouldSerialize(type))
6160
attributes.Add(BuildAttribute(type));
6261
}
6362

src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal sealed class TypeDeclarationGenerator (Preferences prefs)
2020
public string Generate (SolutionInspection inspection)
2121
{
2222
instanced = [..inspection.InstancedInterfaces];
23-
types = inspection.Crawled.OrderBy(GetNamespace).ToArray();
23+
types = inspection.Crawled.Where(FilterCrawled).OrderBy(GetNamespace).ToArray();
2424
for (index = 0; index < types.Length; index++)
2525
DeclareType();
2626
return builder.ToString();
@@ -32,6 +32,11 @@ private Type GetTypeAt (int index)
3232
return type.IsGenericType ? type.GetGenericTypeDefinition() : type;
3333
}
3434

35+
private bool FilterCrawled (Type type)
36+
{
37+
return !IsList(type) && !IsCollection(type) && !IsNullable(type);
38+
}
39+
3540
private void DeclareType ()
3641
{
3742
if (ShouldOpenNamespace()) OpenNamespace();

src/cs/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project>
22

33
<PropertyGroup>
4-
<Version>0.6.1</Version>
4+
<Version>0.6.2</Version>
55
<Authors>Elringus</Authors>
66
<PackageTags>javascript typescript ts js wasm node deno bun interop codegen</PackageTags>
77
<PackageProjectUrl>https://bootsharp.com</PackageProjectUrl>

0 commit comments

Comments
 (0)