diff --git a/src/MemoryPack.Core/PropertyHelper.cs b/src/MemoryPack.Core/PropertyHelper.cs new file mode 100644 index 0000000..24e9aae --- /dev/null +++ b/src/MemoryPack.Core/PropertyHelper.cs @@ -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 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)); + //var instanceParameter = Expression.Parameter(typeOfT, "instance"); + //var valueParameter = Expression.Parameter(typeof(TValue), "value"); + //var callExpr = Expression.Call(instanceParameter, setMethod, valueParameter); + //var lambda = Expression.Lambda>(callExpr, instanceParameter, valueParameter); + //setter = lambda.Compile(); + _cache.TryAdd(key, setter); + } + + ((Action)setter).Invoke(instance, newValue); + } +} diff --git a/src/MemoryPack.Generator/FullyQualifiedNameRewriter.cs b/src/MemoryPack.Generator/FullyQualifiedNameRewriter.cs new file mode 100644 index 0000000..a95f947 --- /dev/null +++ b/src/MemoryPack.Generator/FullyQualifiedNameRewriter.cs @@ -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); + } +} diff --git a/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs b/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs index d17735f..389074b 100644 --- a/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs +++ b/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs @@ -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) { @@ -509,7 +513,7 @@ private string EmitDeserializeBody() id = reader.ReadVarIntUInt32(); if (value == null) { - value = new {{TypeName}}(); + value = {{instanceCreationBody }}; } reader.OptionalState.AddObjectReference(id, value); """; @@ -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}}) @@ -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: @@ -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}")); } @@ -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"; diff --git a/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs b/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs index 0806cd1..5f04680 100644 --- a/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs +++ b/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs @@ -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; } } } @@ -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; } @@ -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; @@ -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 diff --git a/tests/MemoryPack.Tests/CircularReferenceTest.cs b/tests/MemoryPack.Tests/CircularReferenceTest.cs index b9b1f17..8d34d4e 100644 --- a/tests/MemoryPack.Tests/CircularReferenceTest.cs +++ b/tests/MemoryPack.Tests/CircularReferenceTest.cs @@ -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; @@ -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(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(); + } } diff --git a/tests/MemoryPack.Tests/GeneratorTest.cs b/tests/MemoryPack.Tests/GeneratorTest.cs index cb38893..8b66727 100644 --- a/tests/MemoryPack.Tests/GeneratorTest.cs +++ b/tests/MemoryPack.Tests/GeneratorTest.cs @@ -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")); diff --git a/tests/MemoryPack.Tests/Models/CircularReference.cs b/tests/MemoryPack.Tests/Models/CircularReference.cs index 3de88b0..38b58a5 100644 --- a/tests/MemoryPack.Tests/Models/CircularReference.cs +++ b/tests/MemoryPack.Tests/Models/CircularReference.cs @@ -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; @@ -44,8 +40,6 @@ public partial class Employee public List? DirectReports { get; set; } } - - [MemoryPackable(GenerateType.CircularReference, SerializeLayout.Sequential)] public partial class SequentialCircularReference { @@ -53,3 +47,16 @@ public partial class SequentialCircularReference public SequentialCircularReference? Manager { get; set; } public List? 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 DirectReports { get; set; } +} diff --git a/tests/MemoryPack.Tests/Models/Records.cs b/tests/MemoryPack.Tests/Models/Records.cs index d5f5b48..2a9b665 100644 --- a/tests/MemoryPack.Tests/Models/Records.cs +++ b/tests/MemoryPack.Tests/Models/Records.cs @@ -30,6 +30,16 @@ public partial class RequiredType public required string MyProperty2 { get; set; } } +[MemoryPackable] +public partial class RequiredInitOnlyType +{ + public required int MyProperty1 { get; init; } + public required string MyProperty2 { get; init; } + public required string MyProperty3 { get; set; } + public string? MyProperty4 { get; set; } + public string? MyProperty5 { get; init; } +} + [MemoryPackable] public partial struct RequiredType2 { @@ -42,6 +52,21 @@ public void F() } } +[MemoryPackable] +public partial struct RequiredInitOnlyType2 +{ + public required int MyProperty1 { get; init; } + public required string MyProperty2 { get; init; } + public required string MyProperty3 { get; set; } + public string? MyProperty4 { get; set; } + public string? MyProperty5 { get; init; } + + public void F() + { + // new MyRecord() + } +} + #endif [MemoryPackable] diff --git a/tests/MemoryPack.Tests/SourceGeneratorTests/GeneratorDiagnosticsTest.cs b/tests/MemoryPack.Tests/SourceGeneratorTests/GeneratorDiagnosticsTest.cs index 9ba79cb..69b0106 100644 --- a/tests/MemoryPack.Tests/SourceGeneratorTests/GeneratorDiagnosticsTest.cs +++ b/tests/MemoryPack.Tests/SourceGeneratorTests/GeneratorDiagnosticsTest.cs @@ -627,7 +627,7 @@ public partial class Tester """); } - [Fact] + [Fact(Skip = "Obsolete")] public void MEMPACK040_SuppressDefaultInitializationMustBeSettable() { Compile(40, """ diff --git a/tests/MemoryPack.Tests/SourceGeneratorTests/IncrementalGeneratorTest.cs b/tests/MemoryPack.Tests/SourceGeneratorTests/IncrementalGeneratorTest.cs index 87f6a9e..789e900 100644 --- a/tests/MemoryPack.Tests/SourceGeneratorTests/IncrementalGeneratorTest.cs +++ b/tests/MemoryPack.Tests/SourceGeneratorTests/IncrementalGeneratorTest.cs @@ -12,6 +12,29 @@ public class IncrementalGeneratorTest public void Run() { // lang=C#-test + + //TODO: this is just while WIP + var code = """ + using System.Collections.ObjectModel; + using MemoryPack; + + + [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 DirectReports { get; set; } + } + + """; + var k = string.Join(Environment.NewLine, CSharpGeneratorRunner.RunGenerator(code).Item1.SyntaxTrees.Select(x => x.ToString())); + var step1 = """ [MemoryPackable] public partial class MyClass