diff --git a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs index 95916e88..9b1056b7 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs @@ -23,7 +23,53 @@ public void WhenNoSerializableTypesIsEmpty () } [Fact] - public void DoesntSerializeInstancedInteropInterfaces () + public void SerializesTypesFromInteropMethods () + { + AddAssembly(With( + """ + public record RecordA; + public record RecordB; + public record RecordC; + + public class Class + { + [JSInvokable] public static Task A (RecordC c) => default; + [JSFunction] public static RecordB[] B (RecordC[] c) => default; + } + """)); + Execute(); + Contains("[JsonSerializable(typeof(global::RecordA)"); + Contains("[JsonSerializable(typeof(global::RecordB)"); + Contains("[JsonSerializable(typeof(global::RecordC)"); + Contains("[JsonSerializable(typeof(global::RecordA[])"); + Contains("[JsonSerializable(typeof(global::RecordB[])"); + Contains("[JsonSerializable(typeof(global::RecordC[])"); + } + + [Fact] + public void SerializesTypesFromInteropInterfaces () + { + AddAssembly(With( + """ + public record RecordA; + public record RecordB; + public record RecordC; + public interface IExported { void Inv (RecordA a); } + public interface IImported { void Fun (RecordB b); void NotifyEvt(RecordC c); } + + public class Class + { + [JSFunction] public static Task GetImported (IExported arg) => default; + } + """)); + Execute(); + Contains("[JsonSerializable(typeof(global::RecordA)"); + Contains("[JsonSerializable(typeof(global::RecordB)"); + Contains("[JsonSerializable(typeof(global::RecordC)"); + } + + [Fact] + public void DoesntSerializeInstancedInteropInterfacesThemselves () { AddAssembly(With( """ @@ -46,23 +92,32 @@ public class Class DoesNotContain("JsonSerializable"); } - [Fact] // .NET's generator indexes types by short names (w/o namespace) and fails on duplicates. - public void AddsOnlyTopLevelTypesAndCrawledDuplicates () + [Fact] + public void SerializesAllTheCrawledSerializableTypes () { + // .NET's generator indexes types by short names (w/o namespace) and fails on duplicates, so we have to add everything ourselves. + // https://github.com/dotnet/runtime/issues/58938#issuecomment-1306731801 AddAssembly( - With("y", "public struct Struct { public double A { get; set; } }"), - With("n", "public struct Struct { public y.Struct S { get; set; } }"), + With("y", "public enum Enum { A, B }"), + With("y", "public record Struct (double A, ReadonlyStruct[]? B);"), + With("y", "public record ReadonlyStruct (Enum e);"), + With("n", "public struct Struct { public y.Struct S { get; set; } public ReadonlyStruct[]? A { get; set; } }"), With("n", "public readonly struct ReadonlyStruct { public double A { get; init; } }"), With("n", "public readonly record struct ReadonlyRecordStruct(double A);"), With("n", "public record class RecordClass(double A);"), With("n", "public enum Enum { A, B }"), With("n", "public class Foo { public Struct S { get; } public ReadonlyStruct Rs { get; } }"), WithClass("n", "public class Bar : Foo { public ReadonlyRecordStruct Rrs { get; } public RecordClass Rc { get; } }"), - With("n", "public class Baz { public List Bars { get; } public Enum E { get; } }"), - WithClass("n", "[JSInvokable] public static Task GetBaz () => default;")); + With("n", "public class Baz { public List Bars { get; } }"), + WithClass("n", "[JSInvokable] public static Task GetBaz (Enum e) => default;")); Execute(); - Assert.Equal(2, Matches("JsonSerializable").Count); - Contains("[JsonSerializable(typeof(global::n.Baz)"); + Contains("[JsonSerializable(typeof(global::y.Enum)"); + Contains("[JsonSerializable(typeof(global::n.Enum)"); Contains("[JsonSerializable(typeof(global::y.Struct)"); + Contains("[JsonSerializable(typeof(global::n.Struct)"); + Contains("[JsonSerializable(typeof(global::n.ReadonlyStruct)"); + Contains("[JsonSerializable(typeof(global::y.ReadonlyStruct)"); + Contains("[JsonSerializable(typeof(global::n.ReadonlyStruct[])"); + Contains("[JsonSerializable(typeof(global::y.ReadonlyStruct[])"); } } diff --git a/src/cs/Bootsharp.Publish/Common/TypeConverter/TypeCrawler.cs b/src/cs/Bootsharp.Publish/Common/TypeConverter/TypeCrawler.cs index 03198303..aeb29fda 100644 --- a/src/cs/Bootsharp.Publish/Common/TypeConverter/TypeCrawler.cs +++ b/src/cs/Bootsharp.Publish/Common/TypeConverter/TypeCrawler.cs @@ -9,10 +9,11 @@ internal sealed class TypeCrawler public void Crawl (Type type) { if (!ShouldCrawl(type)) return; - type = GetUnderlyingType(type); - if (!crawled.Add(type)) return; - CrawlProperties(type); - CrawlBaseType(type); + var underlyingType = GetUnderlyingType(type); + if (!crawled.Add(underlyingType)) return; + CrawlProperties(underlyingType); + CrawlBaseType(underlyingType); + crawled.Add(type); } private bool ShouldCrawl (Type type) diff --git a/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs b/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs index aa17a838..070939e2 100644 --- a/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs @@ -11,8 +11,8 @@ internal sealed class SerializerGenerator public string Generate (SolutionInspection inspection) { - CollectAttributes(inspection); - CollectDuplicates(inspection); + CollectTopLevel(inspection); + CollectCrawled(inspection); if (attributes.Count == 0) return ""; return $""" @@ -30,7 +30,7 @@ internal partial class SerializerContext : JsonSerializerContext; """; } - private void CollectAttributes (SolutionInspection inspection) + private void CollectTopLevel (SolutionInspection inspection) { var metas = inspection.StaticMethods .Concat(inspection.StaticInterfaces.SelectMany(i => i.Methods)) @@ -53,11 +53,10 @@ private void CollectFromValue (ValueMeta meta) attributes.Add(BuildAttribute(meta.Type)); } - private void CollectDuplicates (SolutionInspection inspection) + private void CollectCrawled (SolutionInspection inspection) { - var names = new HashSet(); - foreach (var type in inspection.Crawled.DistinctBy(t => t.FullName)) - if (ShouldSerialize(type) && !names.Add(type.Name)) + foreach (var type in inspection.Crawled) + if (ShouldSerialize(type)) attributes.Add(BuildAttribute(type)); } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs index 6388eac0..71dd4c8b 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs @@ -20,7 +20,7 @@ internal sealed class TypeDeclarationGenerator (Preferences prefs) public string Generate (SolutionInspection inspection) { instanced = [..inspection.InstancedInterfaces]; - types = inspection.Crawled.OrderBy(GetNamespace).ToArray(); + types = inspection.Crawled.Where(FilterCrawled).OrderBy(GetNamespace).ToArray(); for (index = 0; index < types.Length; index++) DeclareType(); return builder.ToString(); @@ -32,6 +32,11 @@ private Type GetTypeAt (int index) return type.IsGenericType ? type.GetGenericTypeDefinition() : type; } + private bool FilterCrawled (Type type) + { + return !IsList(type) && !IsCollection(type) && !IsNullable(type); + } + private void DeclareType () { if (ShouldOpenNamespace()) OpenNamespace(); diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 679ba8b0..e3217f8d 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.6.1 + 0.6.2 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com