diff --git a/src/MessagePack.SourceGenerator/CodeAnalysis/MemberSerializationInfo.cs b/src/MessagePack.SourceGenerator/CodeAnalysis/MemberSerializationInfo.cs index 0f30d297d..59ba8eb47 100644 --- a/src/MessagePack.SourceGenerator/CodeAnalysis/MemberSerializationInfo.cs +++ b/src/MessagePack.SourceGenerator/CodeAnalysis/MemberSerializationInfo.cs @@ -7,6 +7,8 @@ public record MemberSerializationInfo( bool IsProperty, bool IsWritable, bool IsReadable, + bool IsInitOnly, + bool IsRequired, int IntKey, string StringKey, string Name, @@ -16,6 +18,8 @@ public record MemberSerializationInfo( { private static readonly IReadOnlyCollection PrimitiveTypes = new HashSet(AnalyzerUtilities.PrimitiveTypes); + public string LocalVariableName => $"__{this.Name}__"; + public string GetSerializeMethodString() { if (CustomFormatter is not null) diff --git a/src/MessagePack.SourceGenerator/CodeAnalysis/ObjectSerializationInfo.cs b/src/MessagePack.SourceGenerator/CodeAnalysis/ObjectSerializationInfo.cs index 6fa9936cb..b23d6e503 100644 --- a/src/MessagePack.SourceGenerator/CodeAnalysis/ObjectSerializationInfo.cs +++ b/src/MessagePack.SourceGenerator/CodeAnalysis/ObjectSerializationInfo.cs @@ -1,7 +1,7 @@ // Copyright (c) All contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System.Diagnostics.CodeAnalysis; +using System.Text; using Microsoft.CodeAnalysis; namespace MessagePack.SourceGenerator.CodeAnalysis; @@ -16,6 +16,13 @@ public record ObjectSerializationInfo : ResolverRegisterInfo public required MemberSerializationInfo[] ConstructorParameters { get; init; } + /// + /// Gets the members that are either init-only properties or required (and therefore must appear in an object initializer). + /// + public required MemberSerializationInfo[] InitMembers { get; init; } + + public bool MustDeserializeFieldsFirst => this.ConstructorParameters.Length > 0 || this.InitMembers.Length > 0; + public required bool IsIntKey { get; init; } public required MemberSerializationInfo[] Members { get; init; } @@ -76,6 +83,7 @@ public static ObjectSerializationInfo Create( IncludesPrivateMembers = includesPrivateMembers, GenericTypeParameters = genericTypeParameters, ConstructorParameters = constructorParameters, + InitMembers = members.Where(x => ((x.IsProperty && x.IsInitOnly) || x.IsRequired) && !constructorParameters.Contains(x)).ToArray(), IsIntKey = isIntKey, Members = members, HasIMessagePackSerializationCallbackReceiver = hasIMessagePackSerializationCallbackReceiver, @@ -91,8 +99,47 @@ public static ObjectSerializationInfo Create( public string GetConstructorString() { - var args = string.Join(", ", this.ConstructorParameters.Select(x => "__" + x.Name + "__")); - return $"{this.DataType.GetQualifiedName(Qualifiers.GlobalNamespace, GenericParameterStyle.Identifiers)}({args})"; + StringBuilder builder = new(); + builder.Append(this.DataType.GetQualifiedName(Qualifiers.GlobalNamespace, GenericParameterStyle.Identifiers)); + + builder.Append('('); + + for (int i = 0; i < this.ConstructorParameters.Length; i++) + { + if (i != 0) + { + builder.Append(", "); + } + + builder.Append(this.ConstructorParameters[i].LocalVariableName); + } + + builder.Append(')'); + + if (this.InitMembers.Length > 0) + { + builder.Append(" { "); + + for (int i = 0; i < this.InitMembers.Length; i++) + { + if (i != 0) + { + builder.Append(", "); + } + + // Strictly speaking, we should only be assigning these init-only properties if values for them + // was provided in the deserialized stream. + // However the C# language does not provide a means to do this, so we always assign them. + // https://github.com/dotnet/csharplang/issues/6117 + builder.Append(this.InitMembers[i].Name); + builder.Append(" = "); + builder.Append(this.InitMembers[i].LocalVariableName); + } + + builder.Append(" }"); + } + + return builder.ToString(); } public virtual bool Equals(ObjectSerializationInfo? other) diff --git a/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs b/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs index 125e55188..f36d82c60 100644 --- a/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs +++ b/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs @@ -5,6 +5,7 @@ #pragma warning disable SA1649 // File name should match first type name using System.Collections.Immutable; +using System.Reflection; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -731,20 +732,21 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr continue; } - var isReadable = item.GetMethod is not null; - var isWritable = item.SetMethod is not null; + bool isReadable = item.GetMethod is not null; + bool isWritable = item.SetMethod is not null; if (!isReadable && !isWritable) { continue; } + bool isInitOnly = item.SetMethod?.IsInitOnly is true; AttributeData? keyAttribute = attributes.FirstOrDefault(attributes => attributes.AttributeClass.ApproximatelyEqual(this.typeReferences.KeyAttribute)); string stringKey = keyAttribute?.ConstructorArguments.Length == 1 && keyAttribute.ConstructorArguments[0].Value is string name ? name : item.Name; includesPrivateMembers |= item.GetMethod is not null && !IsAllowedAccessibility(item.GetMethod.DeclaredAccessibility); includesPrivateMembers |= item.SetMethod is not null && !IsAllowedAccessibility(item.SetMethod.DeclaredAccessibility); FormatterDescriptor? specialFormatter = GetSpecialFormatter(item); - MemberSerializationInfo member = new(true, isWritable, isReadable, hiddenIntKey++, stringKey, item.Name, item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Type.ToDisplayString(BinaryWriteFormat), specialFormatter); + MemberSerializationInfo member = new(true, isWritable, isReadable, isInitOnly, item.IsRequired, hiddenIntKey++, stringKey, item.Name, item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Type.ToDisplayString(BinaryWriteFormat), specialFormatter); stringMembers.Add(member.StringKey, (member, item.Type)); if (specialFormatter is null) @@ -768,7 +770,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr string stringKey = keyAttribute?.ConstructorArguments.Length == 1 && keyAttribute.ConstructorArguments[0].Value is string name ? name : item.Name; FormatterDescriptor? specialFormatter = GetSpecialFormatter(item); - MemberSerializationInfo member = new(false, IsWritable: !item.IsReadOnly, IsReadable: true, hiddenIntKey++, stringKey, item.Name, item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Type.ToDisplayString(BinaryWriteFormat), specialFormatter); + MemberSerializationInfo member = new(false, IsWritable: !item.IsReadOnly, IsReadable: true, IsInitOnly: false, item.IsRequired, hiddenIntKey++, stringKey, item.Name, item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Type.ToDisplayString(BinaryWriteFormat), specialFormatter); stringMembers.Add(member.StringKey, (member, item.Type)); if (specialFormatter is null) { @@ -803,8 +805,9 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr continue; } - var isReadable = item.GetMethod is not null; - var isWritable = item.SetMethod is not null; + bool isReadable = item.GetMethod is not null; + bool isWritable = item.SetMethod is not null; + bool isInitOnly = item.SetMethod?.IsInitOnly is true; if (!isReadable && !isWritable) { continue; @@ -870,7 +873,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.KeysMustBeUnique, item.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax().GetLocation())); } - var member = new MemberSerializationInfo(true, isWritable, isReadable, intKey!.Value, item.Name, item.Name, item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Type.ToDisplayString(BinaryWriteFormat), specialFormatter); + var member = new MemberSerializationInfo(true, isWritable, isReadable, isInitOnly, item.IsRequired, intKey!.Value, item.Name, item.Name, item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Type.ToDisplayString(BinaryWriteFormat), specialFormatter); intMembers.Add(member.IntKey, (member, item.Type)); } else if (stringKey is not null) @@ -880,7 +883,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.KeysMustBeUnique, item.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax().GetLocation())); } - var member = new MemberSerializationInfo(true, isWritable, isReadable, hiddenIntKey++, stringKey!, item.Name, item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Type.ToDisplayString(BinaryWriteFormat), specialFormatter); + var member = new MemberSerializationInfo(true, isWritable, isReadable, isInitOnly, item.IsRequired, hiddenIntKey++, stringKey!, item.Name, item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Type.ToDisplayString(BinaryWriteFormat), specialFormatter); stringMembers.Add(member.StringKey, (member, item.Type)); } } @@ -957,7 +960,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.KeysMustBeUnique, item.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax().GetLocation())); } - var member = new MemberSerializationInfo(true, IsWritable: !item.IsReadOnly, IsReadable: true, intKey!.Value, item.Name, item.Name, item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Type.ToDisplayString(BinaryWriteFormat), specialFormatter); + var member = new MemberSerializationInfo(true, IsWritable: !item.IsReadOnly, IsReadable: true, IsInitOnly: false, item.IsRequired, intKey!.Value, item.Name, item.Name, item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Type.ToDisplayString(BinaryWriteFormat), specialFormatter); intMembers.Add(member.IntKey, (member, item.Type)); } else @@ -967,7 +970,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.KeysMustBeUnique, item.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax().GetLocation())); } - var member = new MemberSerializationInfo(true, IsWritable: !item.IsReadOnly, IsReadable: true, hiddenIntKey++, stringKey!, item.Name, item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Type.ToDisplayString(BinaryWriteFormat), specialFormatter); + var member = new MemberSerializationInfo(true, IsWritable: !item.IsReadOnly, IsReadable: true, IsInitOnly: false, item.IsRequired, hiddenIntKey++, stringKey!, item.Name, item.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), item.Type.ToDisplayString(BinaryWriteFormat), specialFormatter); stringMembers.Add(member.StringKey, (member, item.Type)); } } diff --git a/src/MessagePack.SourceGenerator/Transforms/FormatterTemplate.cs b/src/MessagePack.SourceGenerator/Transforms/FormatterTemplate.cs index 42e4c3bba..02f7189aa 100644 --- a/src/MessagePack.SourceGenerator/Transforms/FormatterTemplate.cs +++ b/src/MessagePack.SourceGenerator/Transforms/FormatterTemplate.cs @@ -107,18 +107,18 @@ public virtual string TransformText() this.Write("\t\t\tMsgPack::IFormatterResolver formatterResolver = options.Resolver;\r\n"); } this.Write("\t\t\tvar length = reader.ReadArrayHeader();\r\n"); - var canOverwrite = Info.ConstructorParameters.Length == 0; - if (canOverwrite) { + if (Info.MustDeserializeFieldsFirst) { + foreach (var member in Info.Members) { + this.Write("\t\t\tvar "); + this.Write(this.ToStringHelper.ToStringWithCulture(member.LocalVariableName)); + this.Write(" = default("); + this.Write(this.ToStringHelper.ToStringWithCulture(member.Type)); + this.Write(");\r\n"); + } +} else { this.Write("\t\t\tvar ____result = new "); this.Write(this.ToStringHelper.ToStringWithCulture(Info.GetConstructorString())); this.Write(";\r\n"); - } else { foreach (var member in Info.Members) { - this.Write("\t\t\tvar __"); - this.Write(this.ToStringHelper.ToStringWithCulture(member.Name)); - this.Write("__ = default("); - this.Write(this.ToStringHelper.ToStringWithCulture(member.Type)); - this.Write(");\r\n"); - } } this.Write("\r\n\t\t\tfor (int i = 0; i < length; i++)\r\n\t\t\t{\r\n\t\t\t\tswitch (i)\r\n\t\t\t\t{\r\n"); for (var memberIndex = 0; memberIndex <= Info.MaxKey; memberIndex++) { @@ -127,7 +127,13 @@ public virtual string TransformText() this.Write("\t\t\t\t\tcase "); this.Write(this.ToStringHelper.ToStringWithCulture(member.IntKey)); this.Write(":\r\n"); - if (canOverwrite) { + if (Info.MustDeserializeFieldsFirst) { + this.Write("\t\t\t\t\t\t"); + this.Write(this.ToStringHelper.ToStringWithCulture(member.LocalVariableName)); + this.Write(" = "); + this.Write(this.ToStringHelper.ToStringWithCulture(member.GetDeserializeMethodString())); + this.Write(";\r\n"); + } else { if (member.IsWritable) { this.Write("\t\t\t\t\t\t____result."); this.Write(this.ToStringHelper.ToStringWithCulture(member.Name)); @@ -135,36 +141,28 @@ public virtual string TransformText() this.Write(this.ToStringHelper.ToStringWithCulture(member.GetDeserializeMethodString())); this.Write(";\r\n"); } else { - this.Write("\t\t\t\t\t\t"); - this.Write(this.ToStringHelper.ToStringWithCulture(member.GetDeserializeMethodString())); - this.Write(";\r\n"); + this.Write("\t\t\t\t\t\treader.Skip();\r\n"); } - } else { - this.Write("\t\t\t\t\t\t__"); - this.Write(this.ToStringHelper.ToStringWithCulture(member.Name)); - this.Write("__ = "); - this.Write(this.ToStringHelper.ToStringWithCulture(member.GetDeserializeMethodString())); - this.Write(";\r\n"); } this.Write("\t\t\t\t\t\tbreak;\r\n"); } this.Write("\t\t\t\t\tdefault:\r\n\t\t\t\t\t\treader.Skip();\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n"); - if (!canOverwrite) { + if (Info.MustDeserializeFieldsFirst) { this.Write("\t\t\tvar ____result = new "); this.Write(this.ToStringHelper.ToStringWithCulture(Info.GetConstructorString())); this.Write(";\r\n"); bool memberAssignExists = false; for (var memberIndex = 0; memberIndex <= Info.MaxKey; memberIndex++) { var member = Info.GetMember(memberIndex); - if (member == null || !member.IsWritable || Info.ConstructorParameters.Any(p => p.Equals(member))) { continue; } + if (member == null || !member.IsWritable || member.IsInitOnly || member.IsRequired || Info.ConstructorParameters.Any(p => p.Equals(member))) { continue; } memberAssignExists = true; this.Write("\t\t\tif (length <= "); this.Write(this.ToStringHelper.ToStringWithCulture(memberIndex)); this.Write(")\r\n\t\t\t{\r\n\t\t\t\tgoto MEMBER_ASSIGNMENT_END;\r\n\t\t\t}\r\n\r\n\t\t\t____result."); this.Write(this.ToStringHelper.ToStringWithCulture(member.Name)); - this.Write(" = __"); - this.Write(this.ToStringHelper.ToStringWithCulture(member.Name)); - this.Write("__;\r\n"); + this.Write(" = "); + this.Write(this.ToStringHelper.ToStringWithCulture(member.LocalVariableName)); + this.Write(";\r\n"); } if (memberAssignExists) { this.Write("\r\n\t\tMEMBER_ASSIGNMENT_END:\r\n"); diff --git a/src/MessagePack.SourceGenerator/Transforms/FormatterTemplate.tt b/src/MessagePack.SourceGenerator/Transforms/FormatterTemplate.tt index a1490aacb..665ff7943 100644 --- a/src/MessagePack.SourceGenerator/Transforms/FormatterTemplate.tt +++ b/src/MessagePack.SourceGenerator/Transforms/FormatterTemplate.tt @@ -75,12 +75,12 @@ using MsgPack = global::MessagePack; MsgPack::IFormatterResolver formatterResolver = options.Resolver; <# } #> var length = reader.ReadArrayHeader(); -<# var canOverwrite = Info.ConstructorParameters.Length == 0; - if (canOverwrite) { #> +<# if (Info.MustDeserializeFieldsFirst) { + foreach (var member in Info.Members) { #> + var <#= member.LocalVariableName #> = default(<#= member.Type #>); +<# } +} else { #> var ____result = new <#= Info.GetConstructorString() #>; -<# } else { foreach (var member in Info.Members) { #> - var __<#= member.Name #>__ = default(<#= member.Type #>); -<# } #> <# } #> for (int i = 0; i < length; i++) @@ -91,14 +91,14 @@ using MsgPack = global::MessagePack; var member = Info.GetMember(memberIndex); if (member == null) { continue; } #> case <#= member.IntKey #>: -<# if (canOverwrite) { +<# if (Info.MustDeserializeFieldsFirst) { #> + <#= member.LocalVariableName #> = <#= member.GetDeserializeMethodString() #>; +<# } else { if (member.IsWritable) { #> ____result.<#= member.Name #> = <#= member.GetDeserializeMethodString() #>; <# } else { #> - <#= member.GetDeserializeMethodString() #>; + reader.Skip(); <# } #> -<# } else {#> - __<#= member.Name #>__ = <#= member.GetDeserializeMethodString() #>; <# } #> break; <# } #> @@ -108,19 +108,19 @@ using MsgPack = global::MessagePack; } } -<# if (!canOverwrite) { #> - var ____result = new <#= Info.GetConstructorString() #>; +<# if (Info.MustDeserializeFieldsFirst) { #> + var ____result = new <#= Info.GetConstructorString() #>; <# bool memberAssignExists = false; for (var memberIndex = 0; memberIndex <= Info.MaxKey; memberIndex++) { var member = Info.GetMember(memberIndex); - if (member == null || !member.IsWritable || Info.ConstructorParameters.Any(p => p.Equals(member))) { continue; } + if (member == null || !member.IsWritable || member.IsInitOnly || member.IsRequired || Info.ConstructorParameters.Any(p => p.Equals(member))) { continue; } memberAssignExists = true;#> if (length <= <#= memberIndex #>) { goto MEMBER_ASSIGNMENT_END; } - ____result.<#= member.Name #> = __<#= member.Name #>__; + ____result.<#= member.Name #> = <#= member.LocalVariableName #>; <# } #> <# if (memberAssignExists) { #> diff --git a/src/MessagePack.SourceGenerator/Transforms/StringKey/StringKeyFormatterDeserializeHelper.cs b/src/MessagePack.SourceGenerator/Transforms/StringKey/StringKeyFormatterDeserializeHelper.cs index 107ea1571..bdef72c8b 100644 --- a/src/MessagePack.SourceGenerator/Transforms/StringKey/StringKeyFormatterDeserializeHelper.cs +++ b/src/MessagePack.SourceGenerator/Transforms/StringKey/StringKeyFormatterDeserializeHelper.cs @@ -48,7 +48,7 @@ private static void Assign(StringBuilder buffer, in MemberInfoTuple member, bool } else { - if (!member.IsConstructorParameter) + if (!member.IsConstructorParameter && !member.Info.IsInitOnly && !member.Info.IsRequired) { buffer.Append("__").Append(member.Info.Name).Append("__IsInitialized = true;\r\n").Append(indent); for (var i = 0; i < tabCount; i++) diff --git a/src/MessagePack.SourceGenerator/Transforms/StringKey/StringKeyFormatterTemplate.cs b/src/MessagePack.SourceGenerator/Transforms/StringKey/StringKeyFormatterTemplate.cs index 73f332e66..6ecc763ba 100644 --- a/src/MessagePack.SourceGenerator/Transforms/StringKey/StringKeyFormatterTemplate.cs +++ b/src/MessagePack.SourceGenerator/Transforms/StringKey/StringKeyFormatterTemplate.cs @@ -124,43 +124,44 @@ public virtual string TransformText() this.Write("\t\t\tvar formatterResolver = options.Resolver;\r\n"); } this.Write("\t\t\tvar length = reader.ReadMapHeader();\r\n"); - var canOverwrite = Info.ConstructorParameters.Length == 0; - if (canOverwrite) { + if (Info.MustDeserializeFieldsFirst) { + foreach (var member in Info.Members.Where(x => x.IsWritable || Info.ConstructorParameters.Any(p => p.Equals(x)))) { + // Until C# allows for optionally setting init-only properties (https://github.com/dotnet/csharplang/issues/6117) + // we will unconditionally set them, and thus have no reason to track whether the local variable has been initialized. + if (!member.IsInitOnly && !member.IsRequired && !Info.ConstructorParameters.Any(p => p.Equals(member))) { + this.Write("\t\t\tvar "); + this.Write(this.ToStringHelper.ToStringWithCulture(member.LocalVariableName)); + this.Write("IsInitialized = false;\r\n"); + } + this.Write("\t\t\tvar "); + this.Write(this.ToStringHelper.ToStringWithCulture(member.LocalVariableName)); + this.Write(" = default("); + this.Write(this.ToStringHelper.ToStringWithCulture(member.Type)); + this.Write(");\r\n"); + } + } else { this.Write("\t\t\tvar ____result = new "); this.Write(this.ToStringHelper.ToStringWithCulture(Info.GetConstructorString())); this.Write(";\r\n"); - } else { - foreach (var member in Info.Members.Where(x => x.IsWritable || Info.ConstructorParameters.Any(p => p.Equals(x)))) { - if (Info.ConstructorParameters.All(p => !p.Equals(member))) { - this.Write("\t\t\tvar __"); - this.Write(this.ToStringHelper.ToStringWithCulture(member.Name)); - this.Write("__IsInitialized = false;\r\n"); - } - this.Write("\t\t\tvar __"); - this.Write(this.ToStringHelper.ToStringWithCulture(member.Name)); - this.Write("__ = default("); - this.Write(this.ToStringHelper.ToStringWithCulture(member.Type)); - this.Write(");\r\n"); - } } this.Write("\r\n\t\t\tfor (int i = 0; i < length; i++)\r\n\t\t\t{\r\n\t\t\t\tvar stringKey = global::MessageP" + "ack.Internal.CodeGenHelpers.ReadStringSpan(ref reader);\r\n\t\t\t\tswitch (stringKey.L" + "ength)\r\n\t\t\t\t{\r\n\t\t\t\t\tdefault:\r\n\t\t\t\t\tFAIL:\r\n\t\t\t\t\t reader.Skip();\r\n\t\t\t\t\t continue" + ";\r\n"); - this.Write(this.ToStringHelper.ToStringWithCulture(StringKeyFormatterDeserializeHelper.Classify(Info, " ", canOverwrite))); + this.Write(this.ToStringHelper.ToStringWithCulture(StringKeyFormatterDeserializeHelper.Classify(Info, " ", !Info.MustDeserializeFieldsFirst))); this.Write("\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n"); - if (!canOverwrite) { + if (Info.MustDeserializeFieldsFirst) { this.Write("\t\t\tvar ____result = new "); this.Write(this.ToStringHelper.ToStringWithCulture(Info.GetConstructorString())); this.Write(";\r\n"); - foreach (var member in Info.Members.Where(x => x.IsWritable && !Info.ConstructorParameters.Any(p => p.Equals(x)))) { - this.Write("\t\t\tif (__"); - this.Write(this.ToStringHelper.ToStringWithCulture(member.Name)); - this.Write("__IsInitialized)\r\n\t\t\t{\r\n\t\t\t\t____result."); - this.Write(this.ToStringHelper.ToStringWithCulture(member.Name)); - this.Write(" = __"); + foreach (var member in Info.Members.Where(x => x.IsWritable && !x.IsInitOnly && !x.IsRequired && !Info.ConstructorParameters.Any(p => p.Equals(x)))) { + this.Write("\t\t\tif ("); + this.Write(this.ToStringHelper.ToStringWithCulture(member.LocalVariableName)); + this.Write("IsInitialized)\r\n\t\t\t{\r\n\t\t\t\t____result."); this.Write(this.ToStringHelper.ToStringWithCulture(member.Name)); - this.Write("__;\r\n\t\t\t}\r\n\r\n"); + this.Write(" = "); + this.Write(this.ToStringHelper.ToStringWithCulture(member.LocalVariableName)); + this.Write(";\r\n\t\t\t}\r\n\r\n"); } } } diff --git a/src/MessagePack.SourceGenerator/Transforms/StringKey/StringKeyFormatterTemplate.tt b/src/MessagePack.SourceGenerator/Transforms/StringKey/StringKeyFormatterTemplate.tt index b233052ea..9a1f3f690 100644 --- a/src/MessagePack.SourceGenerator/Transforms/StringKey/StringKeyFormatterTemplate.tt +++ b/src/MessagePack.SourceGenerator/Transforms/StringKey/StringKeyFormatterTemplate.tt @@ -84,16 +84,17 @@ using MsgPack = global::MessagePack; var formatterResolver = options.Resolver; <# } #> var length = reader.ReadMapHeader(); -<# var canOverwrite = Info.ConstructorParameters.Length == 0; - if (canOverwrite) { #> +<# if (Info.MustDeserializeFieldsFirst) { + foreach (var member in Info.Members.Where(x => x.IsWritable || Info.ConstructorParameters.Any(p => p.Equals(x)))) { + // Until C# allows for optionally setting init-only properties (https://github.com/dotnet/csharplang/issues/6117) + // we will unconditionally set them, and thus have no reason to track whether the local variable has been initialized. + if (!member.IsInitOnly && !member.IsRequired && !Info.ConstructorParameters.Any(p => p.Equals(member))) { #> + var <#= member.LocalVariableName #>IsInitialized = false; +<# } #> + var <#= member.LocalVariableName #> = default(<#= member.Type #>); +<# } #> +<# } else { #> var ____result = new <#= Info.GetConstructorString() #>; -<# } else { - foreach (var member in Info.Members.Where(x => x.IsWritable || Info.ConstructorParameters.Any(p => p.Equals(x)))) { #> -<# if (Info.ConstructorParameters.All(p => !p.Equals(member))) { #> - var __<#= member.Name #>__IsInitialized = false; -<# } #> - var __<#= member.Name #>__ = default(<#= member.Type #>); -<# } #> <# } #> for (int i = 0; i < length; i++) @@ -105,16 +106,16 @@ using MsgPack = global::MessagePack; FAIL: reader.Skip(); continue; -<#= StringKeyFormatterDeserializeHelper.Classify(Info, " ", canOverwrite) #> +<#= StringKeyFormatterDeserializeHelper.Classify(Info, " ", !Info.MustDeserializeFieldsFirst) #> } } -<# if (!canOverwrite) { #> +<# if (Info.MustDeserializeFieldsFirst) { #> var ____result = new <#= Info.GetConstructorString() #>; -<# foreach (var member in Info.Members.Where(x => x.IsWritable && !Info.ConstructorParameters.Any(p => p.Equals(x)))) { #> - if (__<#= member.Name #>__IsInitialized) +<# foreach (var member in Info.Members.Where(x => x.IsWritable && !x.IsInitOnly && !x.IsRequired && !Info.ConstructorParameters.Any(p => p.Equals(x)))) { #> + if (<#= member.LocalVariableName #>IsInitialized) { - ____result.<#= member.Name #> = __<#= member.Name #>__; + ____result.<#= member.Name #> = <#= member.LocalVariableName #>; } <# } #> diff --git a/tests/MessagePack.SourceGenerator.ExecutionTests/ExecutionTests.cs b/tests/MessagePack.SourceGenerator.ExecutionTests/ExecutionTests.cs index bad98d065..0274ca59f 100644 --- a/tests/MessagePack.SourceGenerator.ExecutionTests/ExecutionTests.cs +++ b/tests/MessagePack.SourceGenerator.ExecutionTests/ExecutionTests.cs @@ -29,7 +29,7 @@ public void ClassWithPropertiesWithGetterAndSetter() [Fact] public void ClassWithPropertiesWithGetterAndCtor() { - this.AssertRoundtrip(new HasPropertiesWithGetterAndCtor(1, "four")); + this.AssertRoundtrip(new HasPropertiesWithGetterAndCtor(1, "four") { C = 3 }); } [Fact] @@ -59,6 +59,18 @@ public void GeneratedResolverPicksUpCustomResolversAutomatically() this.AssertRoundtrip(new MyCustomType2()); } + [Fact] + public void ClassWithInitProperty() + { + this.AssertRoundtrip(new HasInitProperty { A = 1, B = 4, C = 5 }); + } + + [Fact] + public void ClassWithRequiredMembers() + { + this.AssertRoundtrip(new HasRequiredMembers { A = 1, B = 4, C = 5, D = 6 }); + } + #if !FORCE_MAP_MODE // forced map mode simply doesn't support private fields at all as it only notices internal and public members. [Fact] public void PrivateFieldIsSerialized() diff --git a/tests/MessagePack.SourceGenerator.ExecutionTests/HasInitProperty.cs b/tests/MessagePack.SourceGenerator.ExecutionTests/HasInitProperty.cs new file mode 100644 index 000000000..7dcc2fe2d --- /dev/null +++ b/tests/MessagePack.SourceGenerator.ExecutionTests/HasInitProperty.cs @@ -0,0 +1,15 @@ +// Copyright (c) All contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +[MessagePackObject(false)] +internal record HasInitProperty +{ + [Key(0)] + internal int A { get; set; } + + [Key(1)] + internal int? B { get; init; } + + [Key(2)] + internal int C { get; set; } +} diff --git a/tests/MessagePack.SourceGenerator.ExecutionTests/HasPropertiesWithGetterAndCtor.cs b/tests/MessagePack.SourceGenerator.ExecutionTests/HasPropertiesWithGetterAndCtor.cs index c6b237038..6b14a30de 100644 --- a/tests/MessagePack.SourceGenerator.ExecutionTests/HasPropertiesWithGetterAndCtor.cs +++ b/tests/MessagePack.SourceGenerator.ExecutionTests/HasPropertiesWithGetterAndCtor.cs @@ -15,4 +15,7 @@ internal HasPropertiesWithGetterAndCtor(int a, string? b) A = a; B = b; } + + [Key(2)] + internal int C { get; set; } } diff --git a/tests/MessagePack.SourceGenerator.ExecutionTests/HasRequiredMembers.cs b/tests/MessagePack.SourceGenerator.ExecutionTests/HasRequiredMembers.cs new file mode 100644 index 000000000..026653fd3 --- /dev/null +++ b/tests/MessagePack.SourceGenerator.ExecutionTests/HasRequiredMembers.cs @@ -0,0 +1,18 @@ +// Copyright (c) All contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +[MessagePackObject(false)] +internal record HasRequiredMembers +{ + [Key(0)] + internal int A { get; set; } + + [Key(1)] + internal required int? B { get; set; } + + [Key(2)] + internal required int? C; + + [Key(3)] + internal int D { get; set; } +} diff --git a/tests/MessagePack.SourceGenerator.Tests/CodeAnalysis/MemberSerializationInfoTests.cs b/tests/MessagePack.SourceGenerator.Tests/CodeAnalysis/MemberSerializationInfoTests.cs index 044162bf3..4f8bf2312 100644 --- a/tests/MessagePack.SourceGenerator.Tests/CodeAnalysis/MemberSerializationInfoTests.cs +++ b/tests/MessagePack.SourceGenerator.Tests/CodeAnalysis/MemberSerializationInfoTests.cs @@ -12,6 +12,8 @@ public void Equals_ByValue() true, false, true, + false, + false, 1, "Hi", "name", @@ -22,6 +24,8 @@ public void Equals_ByValue() true, false, true, + false, + false, 1, "Hi", "name", @@ -33,6 +37,8 @@ public void Equals_ByValue() false, false, true, + false, + false, 1, "Hi", "name",