Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add support for required and init-only properties #374

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
40 changes: 40 additions & 0 deletions src/MemoryPack.Core/PropertyHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;

namespace MemoryPack;

public static class PropertyHelper
{
private static readonly ConcurrentDictionary<(Type Type, string FieldName), Delegate> _cache = new();

public static void SetInitOnlyProperty<T, TValue>(T instance, string propertyName, TValue newValue)
{
Type typeOfT = typeof(T);
var key = (typeOfT, propertyName);
if (!_cache.TryGetValue(key, out Delegate? setter))
{
var prop = typeOfT.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (prop is null)
{
throw new ArgumentException($"Property \"{propertyName}\" not found in type \"{typeOfT}\"");
}

var setMethod = prop.GetSetMethod(true);
if (setMethod is null)
{
throw new ArgumentException($"Property \"{propertyName}\" does not have a setter.");
}

setter = setMethod.CreateDelegate(typeof(Action<T, TValue>));
//var instanceParameter = Expression.Parameter(typeOfT, "instance");
//var valueParameter = Expression.Parameter(typeof(TValue), "value");
//var callExpr = Expression.Call(instanceParameter, setMethod, valueParameter);
//var lambda = Expression.Lambda<Action<T, TValue>>(callExpr, instanceParameter, valueParameter);
//setter = lambda.Compile();
_cache.TryAdd(key, setter);
}

((Action<T, TValue>)setter).Invoke(instance, newValue);
}
}
33 changes: 33 additions & 0 deletions src/MemoryPack.Generator/FullyQualifiedNameRewriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace MemoryPack.Generator;

internal class FullyQualifiedNameRewriter : CSharpSyntaxRewriter
{
private readonly SemanticModel _semanticModel;

public FullyQualifiedNameRewriter(SemanticModel semanticModel)
{
_semanticModel = semanticModel;
}

public override SyntaxNode? VisitObjectCreationExpression(ObjectCreationExpressionSyntax node)
{
var typeInfo = _semanticModel.GetTypeInfo(node.Type);
var typeSymbol = typeInfo.Type ?? _semanticModel.GetSymbolInfo(node.Type).Symbol as ITypeSymbol;
if (typeSymbol != null)
{
var fullyQualifiedName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);

var newTypeSyntax = SyntaxFactory.ParseTypeName(fullyQualifiedName)
.WithLeadingTrivia(node.Type.GetLeadingTrivia())
.WithTrailingTrivia(node.Type.GetTrailingTrivia());

node = node.WithType(newTypeSyntax);
}

return base.VisitObjectCreationExpression(node);
}
}
37 changes: 32 additions & 5 deletions src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,10 @@ private string EmitDeserializeBody()
var commentOutInvalidBody = "";
var circularReferenceBody = "";
var circularReferenceBody2 = "";
var instanceCreationBody = Members.Any(x => x.IsRequired) ? $"({TypeName})global::System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(typeof({TypeName}))" : EmitConstructor();
var membersDeclaration = Members
.Where(x => x.Symbol != null)
.Select(x => $" {x.MemberType.FullyQualifiedToString()} __{x.Name}" + (!string.IsNullOrEmpty(x.DefaultInitializer) ? $" = {x.DefaultInitializer};" : ";")).NewLine();

if (isVersionTolerant)
{
Expand Down Expand Up @@ -509,7 +513,7 @@ private string EmitDeserializeBody()
id = reader.ReadVarIntUInt32();
if (value == null)
{
value = new {{TypeName}}();
value = {{instanceCreationBody }};
}
reader.OptionalState.AddObjectReference(id, value);
""";
Expand All @@ -524,7 +528,7 @@ private string EmitDeserializeBody()
{{circularReferenceBody}}
{{readBeginBody}}
{{circularReferenceBody2}}
{{Members.Where(x => x.Symbol != null).Select(x => $" {x.MemberType.FullyQualifiedToString()} __{x.Name};").NewLine()}}
{{membersDeclaration}}

{{(!isVersionTolerant ? "" : "var readCount = " + count + ";")}}
if (count == {{count}})
Expand Down Expand Up @@ -579,7 +583,11 @@ private string EmitDeserializeBody()

SET:
{{(!IsUseEmptyConstructor ? "goto NEW;" : "")}}
/*
{{Members.Where(x => x.IsAssignable).Select(x => $" {(IsUseEmptyConstructor ? "" : "// ")}value.@{x.Name} = __{x.Name};").NewLine()}}
{{Members.Where(x => x.IsInitOnly).Select(x => $" global::MemoryPack.PropertyHelper.SetInitOnlyProperty<{TypeName}, {x.MemberType.FullyQualifiedToString()}>(value, nameof({TypeName}.{x.Name}), __{x.Name});").NewLine()}}
*/
{{Members.Where(x => x.IsAssignable || x.IsInitOnly).Select(x => $" {(IsUseEmptyConstructor ? "" : "// ")}{EmitPropertyAssignValue(x)}").NewLine()}}
goto READ_END;

NEW:
Expand Down Expand Up @@ -957,7 +965,8 @@ string EmitDeserializeConstruction(string indent)
{
// all value is deserialized, __Name is exsits.
return string.Join("," + Environment.NewLine, Members
.Where(x => x is { IsSettable: true, IsConstructorParameter: false, SuppressDefaultInitialization: false })
.Where(x => (x.IsSettable && !x.IsConstructorParameter && !x.SuppressDefaultInitialization) || x.IsRequired)
//.Where(x => x is { IsSettable: true, IsConstructorParameter: false, SuppressDefaultInitialization: false })
.Select(x => $"{indent}@{x.Name} = __{x.Name}"));
}

Expand All @@ -967,13 +976,31 @@ string EmitDeserializeConstructionWithBranching(string indent)
.Select((x, i) => (x, i))
.Where(v => v.x.SuppressDefaultInitialization);

//var lines = GenerateType is GenerateType.VersionTolerant or GenerateType.CircularReference
// ? members.Select(v => $"{indent}if (deltas.Length > {v.i} && deltas[{v.i}] != 0) value.@{v.x.Name} = __{v.x.Name};")
// : members.Select(v => $"{indent}if ({v.i + 1} <= count) value.@{v.x.Name} = __{v.x.Name};");

var lines = GenerateType is GenerateType.VersionTolerant or GenerateType.CircularReference
? members.Select(v => $"{indent}if (deltas.Length > {v.i} && deltas[{v.i}] != 0) value.@{v.x.Name} = __{v.x.Name};")
: members.Select(v => $"{indent}if ({v.i + 1} <= count) value.@{v.x.Name} = __{v.x.Name};");
? members.Select(v => $"{indent}if (deltas.Length > {v.i} && deltas[{v.i}] != 0) {EmitPropertyAssignValue(v.x)}")
: members.Select(v => $"{indent}if ({v.i + 1} <= count) {EmitPropertyAssignValue(v.x)}");

return lines.NewLine();
}

string EmitPropertyAssignValue(MemberMeta meta)
{
if (meta.IsAssignable)
{
return $"value.@{meta.Name} = __{meta.Name};";
}
else if (meta.IsInitOnly)
{
return $"global::MemoryPack.PropertyHelper.SetInitOnlyProperty<{TypeName}, {meta.MemberType.FullyQualifiedToString()}>(value, nameof({TypeName}.{meta.Name}), __{meta.Name});";
}

throw new InvalidOperationException($"Unable to emit property assignation expression on non-assignable member {meta.Name}");
}

string EmitUnionTemplate(IGeneratorContext context)
{
var classOrInterfaceOrRecord = IsRecord ? "record" : (Symbol.TypeKind == TypeKind.Interface) ? "interface" : "class";
Expand Down
34 changes: 27 additions & 7 deletions src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -383,8 +383,8 @@ public bool Validate(TypeDeclarationSyntax syntax, IGeneratorContext context, bo
}
else if (item is { SuppressDefaultInitialization: true, IsAssignable: false })
{
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SuppressDefaultInitializationMustBeSettable, item.GetLocation(syntax), Symbol.Name, item.Name));
noError = false;
//context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SuppressDefaultInitializationMustBeSettable, item.GetLocation(syntax), Symbol.Name, item.Name));
//noError = false;
}
}
}
Expand Down Expand Up @@ -620,9 +620,12 @@ partial class MemberMeta
public bool IsField { get; }
public bool IsProperty { get; }
public bool IsSettable { get; }
public bool IsRequired { get; }
public bool IsAssignable { get; }
public bool IsInitOnly { get; }
public bool IsConstructorParameter { get; }
public string? ConstructorParameterName { get; }
public string? DefaultInitializer { get; }
public int Order { get; }
public bool HasExplicitOrder { get; }
public MemberKind Kind { get; }
Expand Down Expand Up @@ -665,6 +668,25 @@ public MemberMeta(ISymbol symbol, IMethodSymbol? constructor, ReferenceSymbols r
this.IsConstructorParameter = false;
}

var equalsSyntax = symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() switch
{
PropertyDeclarationSyntax property => property.Initializer,
VariableDeclaratorSyntax variable => variable.Initializer,
_ => null
};

if (equalsSyntax is not null)
{
var syntaxTree = equalsSyntax.SyntaxTree;
var semanticModel = references.Compilation.GetSemanticModel(syntaxTree);

var rewrittenInitializer = new FullyQualifiedNameRewriter(semanticModel)
.Visit(equalsSyntax.Value);

DefaultInitializer = rewrittenInitializer.ToString();
}
//DefaultInitializer = equalsSyntax?.Value.ToString();

if (symbol is IFieldSymbol f)
{
IsProperty = false;
Expand All @@ -682,11 +704,9 @@ public MemberMeta(ISymbol symbol, IMethodSymbol? constructor, ReferenceSymbols r
IsProperty = true;
IsField = false;
IsSettable = !p.IsReadOnly;
IsAssignable = IsSettable
#if !ROSLYN3
&& !p.IsRequired
#endif
&& (p.SetMethod != null && !p.SetMethod.IsInitOnly);
IsRequired = p.IsRequired;
IsInitOnly = p.SetMethod is not null && p.SetMethod.IsInitOnly;
IsAssignable = IsSettable && (p.SetMethod != null && !p.SetMethod.IsInitOnly);
MemberType = p.Type;
}
else
Expand Down
83 changes: 77 additions & 6 deletions tests/MemoryPack.Tests/CircularReferenceTest.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
#pragma warning disable CS8602

using MemoryPack.Tests.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Threading.Tasks;

namespace MemoryPack.Tests;

Expand Down Expand Up @@ -151,4 +145,81 @@ public void Sequential()
tylerDeserialized?.DirectReports?[0].Manager.Should().BeSameAs(tylerDeserialized);
}

[Fact]
public void RequiredProperties()
{
CircularReferenceWithRequiredProperties manager = new()
{
FirstName = "Tyler",
LastName = "Stein",
Manager = null,
DirectReports = []
};

CircularReferenceWithRequiredProperties emp1 = new()
{
FirstName = "Adrian",
LastName = "King",
Manager = manager,
DirectReports = []
};
CircularReferenceWithRequiredProperties emp2 = new()
{
FirstName = "Ben",
LastName = "Aston",
Manager = manager,
DirectReports = []
};
CircularReferenceWithRequiredProperties emp3 = new()
{
FirstName = "Emily",
LastName = "Ottoline",
Manager = emp2,
DirectReports = []
};
CircularReferenceWithRequiredProperties emp4 = new()
{
FirstName = "Jaymes",
LastName = "Jaiden",
Manager = emp2,
DirectReports = []
};
manager.DirectReports = [emp1, emp2];
emp2.DirectReports = [emp3, emp4];


var bin = MemoryPackSerializer.Serialize(manager);

CircularReferenceWithRequiredProperties? deserialized = MemoryPackSerializer.Deserialize<CircularReferenceWithRequiredProperties>(bin);

deserialized.Should().NotBeNull();
deserialized!.FirstName.Should().Be("Tyler");
deserialized.LastName.Should().Be("Stein");
deserialized.Manager.Should().BeNull();
deserialized.DirectReports.Should().HaveCount(2);

var dEmp1 = deserialized.DirectReports[0];
dEmp1.FirstName.Should().Be("Adrian");
dEmp1.LastName.Should().Be("King");
dEmp1.Manager.Should().BeSameAs(deserialized);
dEmp1.DirectReports.Should().BeEmpty();

var dEmp2 = deserialized.DirectReports[1];
dEmp2.FirstName.Should().Be("Ben");
dEmp2.LastName.Should().Be("Aston");
dEmp2.Manager.Should().BeSameAs(deserialized);
dEmp2.DirectReports.Should().HaveCount(2);

var dEmp3 = dEmp2.DirectReports[0];
dEmp3.FirstName.Should().Be("Emily");
dEmp3.LastName.Should().Be("Ottoline");
dEmp3.Manager.Should().BeSameAs(dEmp2);
dEmp3.DirectReports.Should().BeEmpty();

var dEmp4 = dEmp2.DirectReports[1];
dEmp4.FirstName.Should().Be("Jaymes");
dEmp4.LastName.Should().Be("Jaiden");
dEmp4.Manager.Should().BeSameAs(dEmp2);
dEmp4.DirectReports.Should().BeEmpty();
}
}
16 changes: 16 additions & 0 deletions tests/MemoryPack.Tests/GeneratorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,23 @@ public void Records()
VerifyEquivalent(new IncludesReferenceStruct { X = 9, Y = "foobarbaz" });
#if NET7_0_OR_GREATER
VerifyEquivalent(new RequiredType { MyProperty1 = 10, MyProperty2 = "hogemogehuga" });
VerifyEquivalent(new RequiredInitOnlyType
{
MyProperty1 = 10,
MyProperty2 = "hogemogehuga",
MyProperty3 = "qwerty",
MyProperty4 = "property4",
MyProperty5 = "property5"
});
VerifyEquivalent(new RequiredType2 { MyProperty1 = 10, MyProperty2 = "hogemogehuga" });
VerifyEquivalent(new RequiredInitOnlyType2
{
MyProperty1 = 10,
MyProperty2 = "hogemogehuga",
MyProperty3 = "qwerty",
MyProperty4 = "property4",
MyProperty5 = "property5"
});
#endif
VerifyEquivalent(new StructWithConstructor1("foo"));
VerifyEquivalent(new MyRecord(10, 20, "haa"));
Expand Down
21 changes: 14 additions & 7 deletions tests/MemoryPack.Tests/Models/CircularReference.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace MemoryPack.Tests.Models;

Expand Down Expand Up @@ -44,12 +40,23 @@ public partial class Employee
public List<Employee>? DirectReports { get; set; }
}



[MemoryPackable(GenerateType.CircularReference, SerializeLayout.Sequential)]
public partial class SequentialCircularReference
{
public string? Name { get; set; }
public SequentialCircularReference? Manager { get; set; }
public List<SequentialCircularReference>? DirectReports { get; set; }
}

[MemoryPackable(GenerateType.CircularReference)]
public partial class CircularReferenceWithRequiredProperties
{
[MemoryPackOrder(0)]
public required string FirstName { get; init; }
[MemoryPackOrder(1)]
public required string LastName { get; set; }
[MemoryPackOrder(2)]
public CircularReferenceWithRequiredProperties? Manager { get; init; }
[MemoryPackOrder(3)]
public required List<CircularReferenceWithRequiredProperties> DirectReports { get; set; }
}
Loading