Repository: MessagePack-CSharp/MessagePack-CSharp

Issue:
MsgPack013 should not fire for formatters with ExcludeFormatterFromSourceGeneratedResolver
A formatter that must be created with a particular configuration and thus cannot have a default constructor will use the `[ExcludeFormatterFromSourceGeneratedResolver]` attribute to exclude itself from the source generated resolver. In which case, MsgPack013 should not flag that formatter as requiring a default constructor.


Hints:

Patch:
diff --git a/sandbox/DynamicCodeDumper/DynamicCodeDumper.csproj b/sandbox/DynamicCodeDumper/DynamicCodeDumper.csproj
index 9a73030a9..ed83e60a6 100644
--- a/sandbox/DynamicCodeDumper/DynamicCodeDumper.csproj
+++ b/sandbox/DynamicCodeDumper/DynamicCodeDumper.csproj
@@ -10,6 +10,9 @@
     <Compile Include="..\..\src\MessagePack.Annotations\Attributes.cs">
       <Link>Code\Attributes.cs</Link>
     </Compile>
+    <Compile Include="..\..\src\MessagePack.Annotations\AnalyzerAttributes.cs">
+      <Link>Code\AnalyzerAttributes.cs</Link>
+    </Compile>
     <Compile Include="..\..\src\MessagePack\BufferWriter.cs">
       <Link>Code\BufferWriter.cs</Link>
     </Compile>
diff --git a/sandbox/SharedData/Class1.cs b/sandbox/SharedData/Class1.cs
index c2e856cb7..b206c02f5 100644
--- a/sandbox/SharedData/Class1.cs
+++ b/sandbox/SharedData/Class1.cs
@@ -100,9 +100,8 @@ public void Serialize(ref MessagePackWriter writer, int value, MessagePackSerial
         }
     }
 
-#pragma warning disable MsgPack013 // no default constructor
+    [ExcludeFormatterFromSourceGeneratedResolver]
     public class OreOreFormatter2 : IMessagePackFormatter<ulong>
-#pragma warning restore MsgPack013 // no default constructor
     {
         public OreOreFormatter2(int x, string y)
         {
@@ -844,9 +843,8 @@ public DynamicArgumentTuple(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5, T6
         }
     }
 
-#pragma warning disable MsgPack013 // no default constructor
+    [ExcludeFormatterFromSourceGeneratedResolver]
     public class DynamicArgumentTupleFormatter<T1, T2, T3, T4, T5, T6, T7, T8, T9> : IMessagePackFormatter<DynamicArgumentTuple<T1, T2, T3, T4, T5, T6, T7, T8, T9>>
-#pragma warning restore MsgPack013 // no default constructor
     {
         private readonly T1 default1;
         private readonly T2 default2;
diff --git a/src/MessagePack.SourceGenerator/Analyzers/MsgPack00xMessagePackAnalyzer.cs b/src/MessagePack.SourceGenerator/Analyzers/MsgPack00xMessagePackAnalyzer.cs
index eaab08734..1c2d5e612 100644
--- a/src/MessagePack.SourceGenerator/Analyzers/MsgPack00xMessagePackAnalyzer.cs
+++ b/src/MessagePack.SourceGenerator/Analyzers/MsgPack00xMessagePackAnalyzer.cs
@@ -312,7 +312,7 @@ private void AnalyzeSymbol(SymbolAnalysisContext context, ReferenceSymbols typeR
                 context.ReportDiagnostic(Diagnostic.Create(CollidingFormatters, declaredSymbol.Locations[0], formattableType.Name.GetQualifiedName(Qualifiers.Namespace)));
             }
 
-            if (formatter.InaccessibleDescriptor is { } inaccessible)
+            if (!formatter.ExcludeFromSourceGeneratedResolver && formatter.InaccessibleDescriptor is { } inaccessible)
             {
                 context.ReportDiagnostic(Diagnostic.Create(inaccessible, declaredSymbol.Locations[0]));
             }
diff --git a/src/MessagePack.SourceGenerator/CodeAnalysis/AnalyzerOptions.cs b/src/MessagePack.SourceGenerator/CodeAnalysis/AnalyzerOptions.cs
index a1016e2cf..478ec30ef 100644
--- a/src/MessagePack.SourceGenerator/CodeAnalysis/AnalyzerOptions.cs
+++ b/src/MessagePack.SourceGenerator/CodeAnalysis/AnalyzerOptions.cs
@@ -41,6 +41,11 @@ public ImmutableHashSet<FormatterDescriptor> KnownFormatters
             bool collisionsEncountered = false;
             foreach (FormatterDescriptor formatter in value)
             {
+                if (formatter.ExcludeFromSourceGeneratedResolver)
+                {
+                    continue;
+                }
+
                 foreach (FormattableType dataType in formatter.FormattableTypes)
                 {
                     if (formattableTypes.ContainsKey(dataType))


Test patch:
diff --git a/tests/MessagePack.SourceGenerator.Tests/ImplicitResolverForCustomFormattersTests.cs b/tests/MessagePack.SourceGenerator.Tests/ImplicitResolverForCustomFormattersTests.cs
index 494d73122..26de2da45 100644
--- a/tests/MessagePack.SourceGenerator.Tests/ImplicitResolverForCustomFormattersTests.cs
+++ b/tests/MessagePack.SourceGenerator.Tests/ImplicitResolverForCustomFormattersTests.cs
@@ -79,6 +79,32 @@ internal class {|MsgPack009:IntFormatter2|} : IMessagePackFormatter<int>
         await VerifyCS.Test.RunDefaultAsync(this.logger, testSource);
     }
 
+    [Fact]
+    public async Task MultipleFunctionalCustomFormattersForType_OnlyOneNonExcluded()
+    {
+        string testSource = """
+            using System;
+            using MessagePack;
+            using MessagePack.Formatters;
+            using MessagePack.Resolvers;
+
+            [ExcludeFormatterFromSourceGeneratedResolver]
+            internal class IntFormatter1 : IMessagePackFormatter<int>
+            {
+                public void Serialize(ref MessagePackWriter writer, int value, MessagePackSerializerOptions options) => writer.Write(value);
+                public int Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) => reader.ReadInt32();
+            }
+
+            internal class IntFormatter2 : IMessagePackFormatter<int>
+            {
+                public void Serialize(ref MessagePackWriter writer, int value, MessagePackSerializerOptions options) => writer.Write(value);
+                public int Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) => reader.ReadInt32();
+            }
+            """;
+
+        await VerifyCS.Test.RunDefaultAsync(this.logger, testSource);
+    }
+
     [Fact]
     public async Task MultipleFunctionalCustomFormattersForType_MultiFormatter()
     {
@@ -135,4 +161,25 @@ public IntFormatter(int value) { } // non-default constructor causes problem
             },
         }.RunDefaultAsync(this.logger);
     }
+
+    [Fact]
+    public async Task CustomFormatterWithoutDefaultConstructor_Excluded()
+    {
+        string testSource = """
+            using System;
+            using MessagePack;
+            using MessagePack.Formatters;
+            using MessagePack.Resolvers;
+
+            [ExcludeFormatterFromSourceGeneratedResolver]
+            internal class IntFormatter : IMessagePackFormatter<int>
+            {
+                public IntFormatter(int value) { } // non-default constructor causes problem
+                public void Serialize(ref MessagePackWriter writer, int value, MessagePackSerializerOptions options) => writer.Write(value);
+                public int Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) => reader.ReadInt32();
+            }
+            """;
+
+        await VerifyCS.Test.RunDefaultAsync(this.logger, testSource);
+    }
 }
diff --git a/tests/MessagePack.SourceGenerator.Tests/Resources/MultipleFunctionalCustomFormattersForType_OnlyOneNonExcluded/MessagePack.GeneratedMessagePackResolver.g.cs b/tests/MessagePack.SourceGenerator.Tests/Resources/MultipleFunctionalCustomFormattersForType_OnlyOneNonExcluded/MessagePack.GeneratedMessagePackResolver.g.cs
new file mode 100644
index 000000000..b54a5cdd1
--- /dev/null
+++ b/tests/MessagePack.SourceGenerator.Tests/Resources/MultipleFunctionalCustomFormattersForType_OnlyOneNonExcluded/MessagePack.GeneratedMessagePackResolver.g.cs
@@ -0,0 +1,63 @@
+﻿// <auto-generated />
+
+#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 {
+
+/// <summary>A MessagePack resolver that uses generated formatters for types in this assembly.</summary>
+partial class GeneratedMessagePackResolver : MsgPack::IFormatterResolver
+{
+	/// <summary>An instance of this resolver that only returns formatters specifically generated for types in this assembly.</summary>
+	public static readonly MsgPack::IFormatterResolver Instance = new GeneratedMessagePackResolver();
+
+	private GeneratedMessagePackResolver()
+	{
+	}
+
+	public MsgPack::Formatters.IMessagePackFormatter<T> GetFormatter<T>()
+	{
+		return FormatterCache<T>.Formatter;
+	}
+
+	private static class FormatterCache<T>
+	{
+		internal static readonly MsgPack::Formatters.IMessagePackFormatter<T> Formatter;
+
+		static FormatterCache()
+		{
+			var f = GeneratedMessagePackResolverGetFormatterHelper.GetFormatter(typeof(T));
+			if (f != null)
+			{
+				Formatter = (MsgPack::Formatters.IMessagePackFormatter<T>)f;
+			}
+		}
+	}
+
+	private static class GeneratedMessagePackResolverGetFormatterHelper
+	{
+		private static readonly global::System.Collections.Generic.Dictionary<global::System.Type, int> closedTypeLookup = new(1)
+		{
+			{ typeof(global::System.Int32), 0 },
+		};
+
+		internal static object GetFormatter(global::System.Type t)
+		{
+			if (closedTypeLookup.TryGetValue(t, out int closedKey))
+			{
+				return closedKey switch
+				{
+					0 => new global::IntFormatter2(),
+					_ => null, // unreachable
+				};
+			}
+
+			return null;
+		}
+	}
+}
+
+}