Skip to content

Commit

Permalink
Add support for primary constructors (#210)
Browse files Browse the repository at this point in the history
Mapping between parameter names and member names only permits
exact case-sensitive match, so lower-case parameters will not map
automatically.
  • Loading branch information
agocke authored Dec 17, 2024
1 parent ccf467b commit 3c353c8
Show file tree
Hide file tree
Showing 14 changed files with 124 additions and 55 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
<Deterministic>true</Deterministic>
<TestTfm>net8.0</TestTfm>
<SerdeAssemblyVersion>0.8.0</SerdeAssemblyVersion>
<SerdePkgVersion>0.8.0-preview1</SerdePkgVersion>
<SerdePkgVersion>0.8.0-preview2</SerdePkgVersion>
</PropertyGroup>
</Project>
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
<PackageVersion Include="NuGetizer" Version="1.2.3" />
<PackageVersion Include="StaticCs" Version="0.2.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.6.0-1.final" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.6.0-1.final" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.6.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.6.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzer.Testing" Version="1.1.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" Version="1.1.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
Expand Down
1 change: 0 additions & 1 deletion src/generator/ConfigOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ internal record TypeOptions
{
public bool DenyUnknownMembers { get; init; } = false;
public MemberFormat MemberFormat { get; init; } = MemberFormat.CamelCase;
public ITypeSymbol? ConstructorSignature { get; init; } = null;
public bool SerializeNull { get; init; } = false;
}

Expand Down
4 changes: 2 additions & 2 deletions src/generator/Diagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ internal enum DiagId
ERR_DoesntImplementInterface = 1,
ERR_TypeNotPartial = 2,
ERR_CantWrapSpecialType = 3,
ERR_CantFindConstructorSignature = 4,
ERR_MissingPrimaryCtor = 4,
ERR_CantFindNestedWrapper = 5,
ERR_WrapperDoesntImplementInterface = 6,
}
Expand All @@ -26,7 +26,7 @@ internal static class Diagnostics
ERR_DoesntImplementInterface => nameof(ERR_DoesntImplementInterface),
ERR_TypeNotPartial => nameof(ERR_TypeNotPartial),
ERR_CantWrapSpecialType => nameof(ERR_CantWrapSpecialType),
ERR_CantFindConstructorSignature => nameof(ERR_CantFindConstructorSignature),
ERR_MissingPrimaryCtor => nameof(ERR_MissingPrimaryCtor),
ERR_CantFindNestedWrapper => nameof(ERR_CantFindNestedWrapper),
ERR_WrapperDoesntImplementInterface => nameof(ERR_WrapperDoesntImplementInterface),
};
Expand Down
53 changes: 18 additions & 35 deletions src/generator/Generator.Deserialize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,7 @@ static string GetReadValueCall(
/// <summary>
/// If the type has a parameterless constructor then we will use that and just set
/// each member in the initializer. If there is no parameterlss constructor, there
/// must be a constructor signature as specified by the ConstructorSignature property
/// in the SerdeTypeOptions.
/// must be a primary constructor.
/// </summary>
private static string GenerateTypeCreation(
GeneratorExecutionContext context,
Expand All @@ -309,56 +308,40 @@ private static string GenerateTypeCreation(
List<DataMemberSymbol> members,
string assignedMask)
{
var targetSignature = SymbolUtilities.GetTypeOptions(type).ConstructorSignature;
var targetTuple = targetSignature as INamedTypeSymbol;
var ctors = type.GetMembers(".ctor");
IMethodSymbol? targetCtor = null;
IMethodSymbol? parameterLessCtor = null;
foreach (var ctorSymbol in ctors)
IMethodSymbol? primaryCtor = null;
IMethodSymbol? parameterlessCtor = null;
if (type is INamedTypeSymbol named)
{
if (ctorSymbol is IMethodSymbol ctorMethod)
foreach (var ctor in named.InstanceConstructors)
{
if (targetTuple is not null && ctorMethod.Parameters.Length == targetTuple.TupleElements.Length)
foreach (var syntaxRef in ctor.DeclaringSyntaxReferences)
{
bool mismatch = false;
for(int i = 0; i < targetTuple.TupleElements.Length; i++)
var syntax = syntaxRef.GetSyntax();
if (syntax is TypeDeclarationSyntax)
{
var elem = targetTuple.TupleElements[i];
var param = ctorMethod.Parameters[i];
if (!elem.Type.Equals(param.Type, SymbolEqualityComparer.Default))
{
mismatch = true;
break;
}
}
if (!mismatch)
{
targetCtor = ctorMethod;
break;
primaryCtor = ctor;
}
}
if (ctorMethod is { Parameters.Length: 0 })
if (ctor.Parameters.Length == 0)
{
parameterLessCtor = ctorMethod;
if (targetSignature is null)
{
break;
}
parameterlessCtor = ctor;
break;
}
}
}
if (targetSignature is not null && targetCtor is null)

if (parameterlessCtor is null && primaryCtor is null)
{
context.ReportDiagnostic(CreateDiagnostic(DiagId.ERR_CantFindConstructorSignature, type.Locations[0]));
return "";
context.ReportDiagnostic(CreateDiagnostic(DiagId.ERR_MissingPrimaryCtor, type.Locations[0]));
return $"var newType = new {typeName}();";
}

var assignmentMembers = new List<DataMemberSymbol>(members);
var assignments = new StringBuilder();
var parameters = new StringBuilder();
if (targetCtor is not null)
if (primaryCtor is not null)
{
foreach (var p in targetCtor.Parameters)
foreach (var p in primaryCtor.Parameters)
{
var index = assignmentMembers.FindIndex(m => m.Name == p.Name);
if (parameters.Length != 0)
Expand Down
5 changes: 0 additions & 5 deletions src/generator/HelpersAndUtilities/SymbolUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,6 @@ internal static TypeOptions GetTypeOptions(ITypeSymbol type)
Type.SpecialType: SpecialType.System_Boolean
}
) => options with { DenyUnknownMembers = (bool)value },
( nameof(TypeOptions.ConstructorSignature),
{
Kind: TypedConstantKind.Type,
}
) => options with { ConstructorSignature = (ITypeSymbol)value },
_ => options
};
}
Expand Down
4 changes: 2 additions & 2 deletions src/generator/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@
<data name="ERR_CantWrapSpecialType" xml:space="preserve">
<value>The type '{0}' can't be automatically wrapped because it is a built-in type.</value>
</data>
<data name="ERR_CantFindConstructorSignature" xml:space="preserve">
<value>Could not find a constructor with the target signature listed by the ConstructorSignature parameter.</value>
<data name="ERR_MissingPrimaryCtor" xml:space="preserve">
<value>Type must have either a primary constructor or a parameterless constructor.</value>
</data>
<data name="ERR_CantFindNestedWrapper" xml:space="preserve">
<value>Could not find nested type named '{0}' inside type '{1}'. The proxied type '{2}' is generic, so the expected proxy is a parent type with Serialize and Deserialize nested proxies.</value>
Expand Down
6 changes: 0 additions & 6 deletions src/serde/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,6 @@ sealed class SerdeTypeOptions : Attribute
/// </summary>
public MemberFormat MemberFormat { get; init; } = MemberFormat.CamelCase;

/// <summary>
/// Pick the constructor used for deserialization. Expects a tuple with the same types as
/// the desired parameter list of the desired constructor.
/// </summary>
public Type? ConstructorSignature { get; init; }

/// <summary>
/// The default behavior for null is to skip serialization. Set this to true to force
/// serialization.
Expand Down
15 changes: 15 additions & 0 deletions test/Serde.Generation.Test/DeserializeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,21 @@ private partial class D
return VerifyDeserialize(src);
}

[Fact]
public Task NoParameterlessOrPrimaryCtor()
{
var src = """
using Serde;
[GenerateDeserialize]
partial class C
{
public int A;
public C(int A) { A = this.A; }
}
""";
return VerifyDeserialize(src);
}

private static Task VerifyDeserialize(
string src,
[CallerMemberName] string caller = "")
Expand Down
1 change: 0 additions & 1 deletion test/Serde.Generation.Test/WrapperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,6 @@ public Task PositionalRecordDeserialize()
using Serde;
[GenerateDeserialize]
[SerdeTypeOptions(ConstructorSignature = typeof((int, string)))]
partial record R(int A, string B);
""";
return VerifyGeneratedCode(src, "R.IDeserialize", """
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//HintName: C.IDeserialize.cs

#nullable enable
using System;
using Serde;

partial class C : Serde.IDeserializeProvider<C>
{
static IDeserialize<C> IDeserializeProvider<C>.DeserializeInstance => CDeserializeProxy.Instance;

sealed class CDeserializeProxy : Serde.IDeserialize<C>
{
C Serde.IDeserialize<C>.Deserialize(IDeserializer deserializer)
{
int _l_a = default !;
byte _r_assignedValid = 0;
var _l_serdeInfo = global::Serde.SerdeInfoProvider.GetInfo<C>();
var typeDeserialize = deserializer.ReadType(_l_serdeInfo);
int _l_index_;
while ((_l_index_ = typeDeserialize.TryReadIndex(_l_serdeInfo, out _)) != IDeserializeType.EndOfType)
{
switch (_l_index_)
{
case 0:
_l_a = typeDeserialize.ReadI32(_l_index_);
_r_assignedValid |= ((byte)1) << 0;
break;
case Serde.IDeserializeType.IndexNotFound:
typeDeserialize.SkipValue();
break;
default:
throw new InvalidOperationException("Unexpected index: " + _l_index_);
}
}

var newType = new C();
return newType;
}

public static readonly CDeserializeProxy Instance = new();
private CDeserializeProxy()
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//HintName: C.ISerdeInfoProvider.cs

#nullable enable
partial class C : Serde.ISerdeInfoProvider
{
static global::Serde.ISerdeInfo global::Serde.ISerdeInfoProvider.SerdeInfo { get; } = Serde.SerdeInfo.MakeCustom(
"C",
typeof(C).GetCustomAttributesData(),
new (string, global::Serde.ISerdeInfo, System.Reflection.MemberInfo)[] {
("a", global::Serde.SerdeInfoProvider.GetInfo<global::Serde.Int32Proxy>(), typeof(C).GetField("A")!)
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[
{
"Id": "CS7036",
"Title": "",
"Severity": "Error",
"WarningLevel": "0",
"Location": "SerdeGenerator/Serde.SerdeImplRoslynGenerator/C.IDeserialize.cs: (34,30)-(34,31)",
"HelpLink": "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS7036)",
"MessageFormat": "There is no argument given that corresponds to the required parameter '{0}' of '{1}'",
"Message": "There is no argument given that corresponds to the required parameter 'A' of 'C.C(int)'",
"Category": "Compiler"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
Diagnostics: [
{
Id: ERR_MissingPrimaryCtor,
Title: ,
Severity: Error,
WarningLevel: 0,
Location: : (2,14)-(2,15),
MessageFormat: Type must have either a primary constructor or a parameterless constructor.,
Message: Type must have either a primary constructor or a parameterless constructor.,
Category: Serde
}
]
}

0 comments on commit 3c353c8

Please sign in to comment.