Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 64 additions & 9 deletions src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RecordA[]> 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<IImported> 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(
"""
Expand All @@ -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<Class.Bar?> Bars { get; } public Enum E { get; } }"),
WithClass("n", "[JSInvokable] public static Task<Baz?> GetBaz () => default;"));
With("n", "public class Baz { public List<Class.Bar?> Bars { get; } }"),
WithClass("n", "[JSInvokable] public static Task<Baz?> 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[])");
}
}
9 changes: 5 additions & 4 deletions src/cs/Bootsharp.Publish/Common/TypeConverter/TypeCrawler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 6 additions & 7 deletions src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
$"""
Expand All @@ -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))
Expand All @@ -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<string>();
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));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/cs/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>

<PropertyGroup>
<Version>0.6.1</Version>
<Version>0.6.2</Version>
<Authors>Elringus</Authors>
<PackageTags>javascript typescript ts js wasm node deno bun interop codegen</PackageTags>
<PackageProjectUrl>https://bootsharp.com</PackageProjectUrl>
Expand Down
Loading