diff --git a/src/MessagePack.SourceGenerator/Analyzers/MsgPack00xMessagePackAnalyzer.cs b/src/MessagePack.SourceGenerator/Analyzers/MsgPack00xMessagePackAnalyzer.cs index 0db4bbda0..f22b5f18f 100644 --- a/src/MessagePack.SourceGenerator/Analyzers/MsgPack00xMessagePackAnalyzer.cs +++ b/src/MessagePack.SourceGenerator/Analyzers/MsgPack00xMessagePackAnalyzer.cs @@ -166,7 +166,7 @@ public class MsgPack00xMessagePackAnalyzer : DiagnosticAnalyzer title: "Deserializing constructors", category: Category, messageFormat: "Deserializing constructor parameter count mismatch", - description: "Constructor parameter count must meet or exceed the number of serialized members or the highest key index.", + description: "The deserializing constructor parameter count must meet or exceed the number of serialized members.", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, helpLinkUri: AnalyzerUtilities.GetHelpLink(DeserializingConstructorId)); diff --git a/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs b/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs index 4a84b7afa..80d24ad91 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.ComponentModel; using System.Reflection; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; @@ -682,6 +683,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr { var isClass = !formattedType.IsValueType; bool nestedFormatterRequired = false; + List nestedFormatterRequiredIfPropertyIsNotSetByDeserializingCtor = new(); AttributeData? contractAttr = formattedType.GetAttributes().FirstOrDefault(x => x.AttributeClass.ApproximatelyEqual(this.typeReferences.MessagePackObjectAttribute)); @@ -875,7 +877,10 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr nonPublicMembersAreSerialized |= (item.DeclaredAccessibility & Accessibility.Public) != Accessibility.Public; nestedFormatterRequired |= item.GetMethod is not null && IsPartialTypeRequired(item.GetMethod.DeclaredAccessibility); - nestedFormatterRequired |= item.SetMethod is not null && IsPartialTypeRequired(item.SetMethod.DeclaredAccessibility); + if (item.SetMethod is not null && IsPartialTypeRequired(item.SetMethod.DeclaredAccessibility)) + { + nestedFormatterRequiredIfPropertyIsNotSetByDeserializingCtor.Add(item); + } var intKey = key is { Value: int intKeyValue } ? intKeyValue : default(int?); var stringKey = key is { Value: string stringKeyValue } ? stringKeyValue : default; @@ -1062,6 +1067,9 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr nestedFormatterRequired |= IsPartialTypeRequired(ctor.DeclaredAccessibility); var constructorLookupDictionary = stringMembers.ToLookup(x => x.Value.Info.Name, x => x, StringComparer.OrdinalIgnoreCase); + IReadOnlyDictionary ctorParamIndexIntMembersDictionary = intMembers + .OrderBy(x => x.Key).Select((x, i) => (Key: x.Value, Index: i)) + .ToDictionary(x => x.Index, x => x.Key); do { constructorParameters.Clear(); @@ -1070,7 +1078,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr { if (isIntKey) { - if (intMembers.TryGetValue(ctorParamIndex, out (MemberSerializationInfo Info, ITypeSymbol TypeSymbol) member)) + if (ctorParamIndexIntMembersDictionary.TryGetValue(ctorParamIndex, out (MemberSerializationInfo Info, ITypeSymbol TypeSymbol) member)) { if (this.compilation.ClassifyConversion(member.TypeSymbol, item.Type) is { IsImplicit: true } && member.Info.IsReadable) { @@ -1204,6 +1212,13 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr return null; } + // If any property had a private setter and does not appear in the deserializing constructor signature, + // we'll need a nested formatter. + foreach (IPropertySymbol property in nestedFormatterRequiredIfPropertyIsNotSetByDeserializingCtor) + { + nestedFormatterRequired |= !constructorParameters.Any(m => m.Name == property.Name); + } + if (nestedFormatterRequired && nonPublicMembersAreSerialized) { // If the data type or any nesting types are not declared with partial, we cannot emit the formatter as a nested type within the data type diff --git a/tests/MessagePack.SourceGenerator.ExecutionTests/DeserializingConstructorStartsWithIdx1.cs b/tests/MessagePack.SourceGenerator.ExecutionTests/DeserializingConstructorStartsWithIdx1.cs new file mode 100644 index 000000000..d0f0857b9 --- /dev/null +++ b/tests/MessagePack.SourceGenerator.ExecutionTests/DeserializingConstructorStartsWithIdx1.cs @@ -0,0 +1,14 @@ +// Copyright (c) All contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +[MessagePackObject] +public class DeserializingConstructorStartsWithIdx1 : IEquatable +{ + [Key(1)] // 1 instead of 0. Works on v2. Test case from https://github.com/MessagePack-CSharp/MessagePack-CSharp/issues/1993 + public string Name { get; } + + [SerializationConstructor] + public DeserializingConstructorStartsWithIdx1(string name) => Name = name; + + public bool Equals(DeserializingConstructorStartsWithIdx1? other) => other is not null && this.Name == other.Name; +} diff --git a/tests/MessagePack.SourceGenerator.ExecutionTests/ExecutionTests.cs b/tests/MessagePack.SourceGenerator.ExecutionTests/ExecutionTests.cs index 320838c58..a3dc5c411 100644 --- a/tests/MessagePack.SourceGenerator.ExecutionTests/ExecutionTests.cs +++ b/tests/MessagePack.SourceGenerator.ExecutionTests/ExecutionTests.cs @@ -64,6 +64,12 @@ public void ClassWithRequiredMembers() this.AssertRoundtrip(new HasRequiredMembers { A = 1, B = 4, C = 5, D = 6 }); } + [Fact] + public void DeserializingConstructorStartsWithIdx1() + { + this.AssertRoundtrip(new DeserializingConstructorStartsWithIdx1("foo")); + } + #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.Tests/GenerationTests.cs b/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs index bdea9aee6..05e7a8c9d 100644 --- a/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs +++ b/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs @@ -672,4 +672,24 @@ class A { ExpectedDiagnostics = { DiagnosticResult.CompilerError("MsgPack007").WithLocation(0) }, }.RunDefaultAsync(this.testOutputHelper); } + + [Fact] + public async Task NoPrivateAccessNeeded() + { + // Test case came from https://github.com/MessagePack-CSharp/MessagePack-CSharp/issues/1993 + string testSource = /* lang=c#-test */ """ + using MessagePack; + + [MessagePackObject] + public class Test3 + { + [Key(0)] + public string Name { get; private set; } + + [SerializationConstructor] + public Test3(string name) => Name = name; + } + """; + await VerifyCS.Test.RunDefaultAsync(this.testOutputHelper, testSource); + } } diff --git a/tests/MessagePack.SourceGenerator.Tests/Resources/NoPrivateAccessNeeded/Formatters.MessagePack.GeneratedMessagePackResolver.Test3Formatter.g.cs b/tests/MessagePack.SourceGenerator.Tests/Resources/NoPrivateAccessNeeded/Formatters.MessagePack.GeneratedMessagePackResolver.Test3Formatter.g.cs new file mode 100644 index 000000000..145490a71 --- /dev/null +++ b/tests/MessagePack.SourceGenerator.Tests/Resources/NoPrivateAccessNeeded/Formatters.MessagePack.GeneratedMessagePackResolver.Test3Formatter.g.cs @@ -0,0 +1,59 @@ +// + +#pragma warning disable 618, 612, 414, 168, CS1591, SA1129, SA1309, SA1312, SA1403, SA1649 + +#pragma warning disable CS8669 // We may leak nullable annotations into generated code. + +using MsgPack = global::MessagePack; + +namespace MessagePack { +partial class GeneratedMessagePackResolver { + + internal sealed class Test3Formatter : MsgPack::Formatters.IMessagePackFormatter + { + + public void Serialize(ref MsgPack::MessagePackWriter writer, global::Test3 value, MsgPack::MessagePackSerializerOptions options) + { + if (value == null) + { + writer.WriteNil(); + return; + } + + MsgPack::IFormatterResolver formatterResolver = options.Resolver; + writer.WriteArrayHeader(1); + MsgPack::FormatterResolverExtensions.GetFormatterWithVerify(formatterResolver).Serialize(ref writer, value.Name, options); + } + + public global::Test3 Deserialize(ref MsgPack::MessagePackReader reader, MsgPack::MessagePackSerializerOptions options) + { + if (reader.TryReadNil()) + { + return null; + } + + options.Security.DepthStep(ref reader); + MsgPack::IFormatterResolver formatterResolver = options.Resolver; + var length = reader.ReadArrayHeader(); + var __Name__ = default(string); + + for (int i = 0; i < length; i++) + { + switch (i) + { + case 0: + __Name__ = MsgPack::FormatterResolverExtensions.GetFormatterWithVerify(formatterResolver).Deserialize(ref reader, options); + break; + default: + reader.Skip(); + break; + } + } + + var ____result = new global::Test3(__Name__); + reader.Depth--; + return ____result; + } + } +} +} diff --git a/tests/MessagePack.SourceGenerator.Tests/Resources/NoPrivateAccessNeeded/MessagePack.GeneratedMessagePackResolver.g.cs b/tests/MessagePack.SourceGenerator.Tests/Resources/NoPrivateAccessNeeded/MessagePack.GeneratedMessagePackResolver.g.cs new file mode 100644 index 000000000..c7531dfa5 --- /dev/null +++ b/tests/MessagePack.SourceGenerator.Tests/Resources/NoPrivateAccessNeeded/MessagePack.GeneratedMessagePackResolver.g.cs @@ -0,0 +1,63 @@ +// + +#pragma warning disable 618, 612, 414, 168, CS1591, SA1129, SA1309, SA1312, SA1403, SA1649 + +using MsgPack = global::MessagePack; + +[assembly: MsgPack::Internal.GeneratedAssemblyMessagePackResolverAttribute(typeof(MessagePack.GeneratedMessagePackResolver), 3, 0)] + +namespace MessagePack { + +/// A MessagePack resolver that uses generated formatters for types in this assembly. +partial class GeneratedMessagePackResolver : MsgPack::IFormatterResolver +{ + /// An instance of this resolver that only returns formatters specifically generated for types in this assembly. + public static readonly MsgPack::IFormatterResolver Instance = new GeneratedMessagePackResolver(); + + private GeneratedMessagePackResolver() + { + } + + public MsgPack::Formatters.IMessagePackFormatter GetFormatter() + { + return FormatterCache.Formatter; + } + + private static class FormatterCache + { + internal static readonly MsgPack::Formatters.IMessagePackFormatter Formatter; + + static FormatterCache() + { + var f = GeneratedMessagePackResolverGetFormatterHelper.GetFormatter(typeof(T)); + if (f != null) + { + Formatter = (MsgPack::Formatters.IMessagePackFormatter)f; + } + } + } + + private static class GeneratedMessagePackResolverGetFormatterHelper + { + private static readonly global::System.Collections.Generic.Dictionary closedTypeLookup = new global::System.Collections.Generic.Dictionary(1) + { + { typeof(global::Test3), 0 }, + }; + + internal static object GetFormatter(global::System.Type t) + { + if (closedTypeLookup.TryGetValue(t, out int closedKey)) + { + switch (closedKey) + { + case 0: return new global::MessagePack.GeneratedMessagePackResolver.Test3Formatter(); + default: return null; // unreachable + }; + } + + return null; + } + } +} + +}