Repository: MessagePack-CSharp/MessagePack-CSharp

Issue:
We need a proper way to exclude a custom formatter from automatic inclusion in the resolver
Since #1739, custom formatters will get included in the resolver. But that breaks the custom formatter that is only intended for explicit opt-in via the `MessagePackFormatterAttribute` applied to a particular member. 
We need a `IgnoreFormatterAttribute` or something that the source generator will recognize and honor by not adding the formatter to the resolver.


Hints:

Patch:
diff --git a/src/MessagePack.SourceGenerator/CodeAnalysis/AnalyzerOptions.cs b/src/MessagePack.SourceGenerator/CodeAnalysis/AnalyzerOptions.cs
index 337955c14..d607c995c 100644
--- a/src/MessagePack.SourceGenerator/CodeAnalysis/AnalyzerOptions.cs
+++ b/src/MessagePack.SourceGenerator/CodeAnalysis/AnalyzerOptions.cs
@@ -189,6 +189,8 @@ public static bool TryCreate(INamedTypeSymbol type, [NotNullWhen(true)] out Cust
                 CodeAnalysisUtilities.FindInaccessibleTypes(type).Any() ? MsgPack00xMessagePackAnalyzer.InaccessibleFormatterType :
                 instanceProvidingMember is null ? MsgPack00xMessagePackAnalyzer.InaccessibleFormatterInstance :
                 null,
+            ExcludeFromSourceGeneratedResolver =
+                type.GetAttributes().Any(a => a.AttributeClass?.Name == Constants.ExcludeFormatterFromSourceGeneratedResolverAttributeName && a.AttributeClass?.ContainingNamespace.Name == Constants.AttributeNamespace),
         };
 
         return true;
@@ -196,6 +198,8 @@ public static bool TryCreate(INamedTypeSymbol type, [NotNullWhen(true)] out Cust
 
     public DiagnosticDescriptor? InaccessibleDescriptor { get; init; }
 
+    public bool ExcludeFromSourceGeneratedResolver { get; init; }
+
     public virtual bool Equals(CustomFormatter other)
     {
         return this.Name.Equals(other.Name)
diff --git a/src/MessagePack.SourceGenerator/MessagePackGenerator.cs b/src/MessagePack.SourceGenerator/MessagePackGenerator.cs
index fc412a67f..f9b413f6d 100644
--- a/src/MessagePack.SourceGenerator/MessagePackGenerator.cs
+++ b/src/MessagePack.SourceGenerator/MessagePackGenerator.cs
@@ -31,7 +31,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
             predicate: static (node, ct) => node is ClassDeclarationSyntax { BaseList.Types.Count: > 0 },
             transform: (ctxt, ct) =>
             {
-                return ctxt.SemanticModel.GetDeclaredSymbol(ctxt.Node, ct) is INamedTypeSymbol symbol && CustomFormatter.TryCreate(symbol, out CustomFormatter? formatter)
+                return ctxt.SemanticModel.GetDeclaredSymbol(ctxt.Node, ct) is INamedTypeSymbol symbol
+                    && CustomFormatter.TryCreate(symbol, out CustomFormatter? formatter)
+                    && !formatter.ExcludeFromSourceGeneratedResolver
                     ? formatter
                     : null;
             }).Collect();
diff --git a/src/MessagePack.SourceGenerator/Utils/Constants.cs b/src/MessagePack.SourceGenerator/Utils/Constants.cs
index b2d26c6cd..28ea3456c 100644
--- a/src/MessagePack.SourceGenerator/Utils/Constants.cs
+++ b/src/MessagePack.SourceGenerator/Utils/Constants.cs
@@ -9,6 +9,7 @@ internal static class Constants
     internal const string CompositeResolverAttributeName = "CompositeResolverAttribute";
     internal const string GeneratedMessagePackResolverAttributeName = "GeneratedMessagePackResolverAttribute";
     internal const string MessagePackKnownFormatterAttributeName = "MessagePackKnownFormatterAttribute";
+    internal const string ExcludeFormatterFromSourceGeneratedResolverAttributeName = "ExcludeFormatterFromSourceGeneratedResolverAttribute";
     internal const string MessagePackAssumedFormattableAttributeName = "MessagePackAssumedFormattableAttribute";
     internal const string MessagePackObjectAttributeName = "MessagePackObjectAttribute";
     internal const string MessagePackUnionAttributeName = "UnionAttribute";
diff --git a/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/Annotations/AnalyzerAttributes.cs b/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/Annotations/AnalyzerAttributes.cs
index 2b6f6099f..4a5c8888f 100644
--- a/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/Annotations/AnalyzerAttributes.cs
+++ b/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/Annotations/AnalyzerAttributes.cs
@@ -55,4 +55,18 @@ public MessagePackAssumedFormattableAttribute(Type formattableType)
         /// </summary>
         public Type FormattableType { get; }
     }
+
+    /// <summary>
+    /// Causes the source generated resolver, which typically includes all implementations of <c>IMessagePackFormatter&lt;T&gt;</c>,
+    /// to exclude this particular formatter.
+    /// </summary>
+    /// <remarks>
+    /// This is useful when the formatter is intended for special case members,
+    /// which may apply the <see cref="MessagePackFormatterAttribute"/> to select the private formatter.
+    /// </remarks>
+    [AttributeUsage(AttributeTargets.Class)]
+    [Conditional("NEVERDEFINED")]
+    public class ExcludeFormatterFromSourceGeneratedResolverAttribute : Attribute
+    {
+    }
 }


Test patch:
diff --git a/tests/MessagePack.SourceGenerator.ExecutionTests/ExcludedCustomFormatter.cs b/tests/MessagePack.SourceGenerator.ExecutionTests/ExcludedCustomFormatter.cs
new file mode 100644
index 000000000..6b0dcd0f8
--- /dev/null
+++ b/tests/MessagePack.SourceGenerator.ExecutionTests/ExcludedCustomFormatter.cs
@@ -0,0 +1,34 @@
+﻿// Copyright (c) All contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using MessagePack.Formatters;
+
+public class ExcludedCustomFormatter
+{
+    [Fact]
+    public void ExcludedFormatterIsIgnored()
+    {
+        // This would normally succeed because of our custom formatter and the auto-generated resolver,
+        // but because of the attribute applied to the formatter, it should not be included in the resolver,
+        // and thus our custom type should fail to serialize as an unknown type.
+        Assert.Throws<MessagePackSerializationException>(
+            () => MessagePackSerializer.Serialize(default(CustomType), MessagePackSerializerOptions.Standard));
+    }
+
+    internal struct CustomType;
+
+    [ExcludeFormatterFromSourceGeneratedResolver]
+    internal class CustomFormatter : IMessagePackFormatter<CustomType>
+    {
+        public CustomType Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
+        {
+            reader.Skip();
+            return default;
+        }
+
+        public void Serialize(ref MessagePackWriter writer, CustomType value, MessagePackSerializerOptions options)
+        {
+            writer.WriteNil();
+        }
+    }
+}
diff --git a/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs b/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs
index d83633313..187b4cf2e 100644
--- a/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs
+++ b/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs
@@ -545,4 +545,22 @@ public void Serialize(ref MessagePackWriter writer, A value, MessagePackSerializ
             """;
         await VerifyCS.Test.RunDefaultAsync(this.testOutputHelper, testSource);
     }
+
+    [Fact]
+    public async Task ExcludeFormatterFromSourceGeneratedResolver()
+    {
+        string testSource = """
+            using MessagePack;
+            using MessagePack.Formatters;
+            class A {}
+            [ExcludeFormatterFromSourceGeneratedResolver]
+            class F : IMessagePackFormatter<A> {
+                public void Serialize(ref MessagePackWriter writer, A value, MessagePackSerializerOptions options) {}
+                public A Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) => default;
+            }
+            """;
+
+        // This test is *not* expected to produce any generated files.
+        await VerifyCS.Test.RunDefaultAsync(this.testOutputHelper, testSource);
+    }
 }