Repository: MessagePack-CSharp/MessagePack-CSharp

Issue:
custom formatters code source generator error
Thank you for your hard work.

I was using 2.5.172 originally, but when I upgraded to 3.0.134-beta,

the parts using custom formatters stopped working.

I thought it might be a problem with how I was using it, so I tried using the sample built-in formatters as shown below, but it doesn't seem to work either.

```
using MessagePack;
using MessagePack.Formatters;
using System;

namespace SP.Runtime
{
	[MessagePackObject]
	public partial class SampleObject
	{
		[Key(0)] public string  Name     { get; set; }
		[Key(1)] public int At { get; set; }

		[Key(2)] [MessagePackFormatter(typeof(ValueTupleFormatter<string>))]
        		private ValueTuple<string> TupleTest;
	}

	public sealed class ValueTupleFormatter<T1> : IMessagePackFormatter<ValueTuple<T1>>
	{
		public void Serialize(ref MessagePackWriter writer, ValueTuple<T1> value, MessagePackSerializerOptions options)
		{
			writer.WriteArrayHeader(1);

			IFormatterResolver resolver = options.Resolver;
			resolver.GetFormatterWithVerify<T1>().Serialize(ref writer, value.Item1, options);
		}

		public ValueTuple<T1> Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
		{
			if (reader.IsNil)
			{
				throw new MessagePackSerializationException("Nil");
				//throw MessagePackSerializationException.ThrowUnexpectedNilWhileDeserializing<ValueTuple<T1>>();
			}
			else
			{
				var count = reader.ReadArrayHeader();
				if (count != 1)
				{
					throw new MessagePackSerializationException("Invalid ValueTuple count");
				}

				IFormatterResolver resolver = options.Resolver;
				options.Security.DepthStep(ref reader);
				try
				{
					T1 item1 = resolver.GetFormatterWithVerify<T1>().Deserialize(ref reader, options);

					return new ValueTuple<T1>(item1);
				}
				finally
				{
					reader.Depth--;
				}
			}
		}
	}
}

```

.g.cs
```
private readonly global::MessagePack.Formatters.TupleFormatter<string> __TupleTestCustomFormatter__ = new global::MessagePack.Formatters.TupleFormatter<T1>();
```

Unity 6000.0.10f1
Message Pack v3.0.134-beta


Hints:
> the parts using custom formatters stopped working.

Can you please provide more detail? What "stopped working"? Did you get a compile error? Runtime error? Did it run but serialize as something unexpected? Please provide any error messages you see.

> I thought it might be a problem with how I was using it, so I tried using the sample built-in formatters as shown below, but it doesn't seem to work either.

It looks like your code snippet is only for the second thing you tried. Providing code for the *first* think you tried would be helpful.
Your code snippet uses `Tuple<string>` but then defines a formatter for `ValueTuple<T>`, which doesn't match and makes me suspicious that the failure is on your end.
And again, please tell me more about what doesn't work.
And BTW, I don't use unity. So if you can provide a sample that doesn't involve unity-specific code, I'm far more likely to be able to diagnose and fix the problem.
Sorry

This is not a hang but a compilation error

I'm very sorry for the mistake in the above description

I have corrected the above description

I have also updated it to remove any references to Unity

I will also share the auto-output code that caused the compilation error

Do you understand the reason?

```
// <auto-generated />

#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 SP.Runtime {
partial class SampleObject {

	internal sealed class SampleObjectFormatter : MsgPack::Formatters.IMessagePackFormatter<global::SP.Runtime.SampleObject>
	{
		private readonly global::SP.Runtime.ValueTupleFormatter<string> __TupleTestCustomFormatter__ = new global::SP.Runtime.ValueTupleFormatter<T1>();

		public void Serialize(ref MsgPack::MessagePackWriter writer, global::SP.Runtime.SampleObject value, MsgPack::MessagePackSerializerOptions options)
		{
			if (value == null)
			{
				writer.WriteNil();
				return;
			}

			MsgPack::IFormatterResolver formatterResolver = options.Resolver;
			writer.WriteArrayHeader(3);
			MsgPack::FormatterResolverExtensions.GetFormatterWithVerify<string>(formatterResolver).Serialize(ref writer, value.Name, options);
			writer.Write(value.At);
			this.__TupleTestCustomFormatter__.Serialize(ref writer, value.TupleTest, options);
		}

		public global::SP.Runtime.SampleObject 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 ____result = new global::SP.Runtime.SampleObject();

			for (int i = 0; i < length; i++)
			{
				switch (i)
				{
					case 0:
						____result.Name = MsgPack::FormatterResolverExtensions.GetFormatterWithVerify<string>(formatterResolver).Deserialize(ref reader, options);
						break;
					case 1:
						____result.At = reader.ReadInt32();
						break;
					case 2:
						____result.TupleTest = this.__TupleTestCustomFormatter__.Deserialize(ref reader, options);
						break;
					default:
						reader.Skip();
						break;
				}
			}

			reader.Depth--;
			return ____result;
		}
	}
}
}

```

You _still_ haven't given me the compile error message.  
A good bug report includes repro instructions, what you expected, and what you actually saw *in detail*.

That's why our bug template looks like this:
![image](https://github.com/user-attachments/assets/0426383e-6fe7-406f-8a5c-b2edc0d75428)

I was able to repro the problem to get the error message myself. This is what would have been great to include in your original bug description:

> 1>C:\temp\ClassLibrary3\ClassLibrary3\obj\Debug\net8.0\MessagePack.SourceGenerator\MessagePack.SourceGenerator.MessagePackGenerator\Formatters.SP.Runtime.SampleObject.SampleObjectFormatter.g.cs(14,141,14,143): error CS0246: The type or namespace name 'T1' could not be found (are you missing a using directive or an assembly reference?)

L14 of the generated file:

```cs
private readonly global::SP.Runtime.ValueTupleFormatter<string> __TupleTestCustomFormatter__ = new global::SP.Runtime.ValueTupleFormatter<T1>();
```

**Analysis**

The custom type's generated formatter uses the type parameter T1 as the type argument for the custom `ValueTupleFormatter<T1>` instead of the type argument given in the `MessagePackFormatterAttribute`, which was given `ValueTupleFormatter<string>`.



Patch:
diff --git a/src/MessagePack.SourceGenerator/CodeAnalysis/AnalyzerOptions.cs b/src/MessagePack.SourceGenerator/CodeAnalysis/AnalyzerOptions.cs
index 4832023ec..f117ee557 100644
--- a/src/MessagePack.SourceGenerator/CodeAnalysis/AnalyzerOptions.cs
+++ b/src/MessagePack.SourceGenerator/CodeAnalysis/AnalyzerOptions.cs
@@ -258,8 +258,8 @@ public static bool TryCreate(INamedTypeSymbol type, [NotNullWhen(true)] out Form
     public bool ExcludeFromSourceGeneratedResolver { get; init; }
 
     public string InstanceExpression => this.InstanceProvidingMember == ".ctor"
-        ? $"new {this.Name.GetQualifiedName()}()"
-        : $"{this.Name.GetQualifiedName()}.{this.InstanceProvidingMember}";
+        ? $"new {this.Name.GetQualifiedName(genericStyle: GenericParameterStyle.Arguments)}()"
+        : $"{this.Name.GetQualifiedName(genericStyle: GenericParameterStyle.Arguments)}.{this.InstanceProvidingMember}";
 
     public virtual bool Equals(FormatterDescriptor? other)
     {


Test patch:
diff --git a/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs b/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs
index fd305d5a7..ab53b8de5 100644
--- a/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs
+++ b/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs
@@ -865,4 +865,37 @@ public List<A> Deserialize(ref MessagePackReader reader, MessagePackSerializerOp
             """;
         await VerifyCS.Test.RunDefaultAsync(this.testOutputHelper, testSource);
     }
+
+    [Fact]
+    public async Task GenericCustomFormatter()
+    {
+        string testSource = /* lang=c#-test */ """
+            using System;
+            using MessagePack;
+            using MessagePack.Formatters;
+
+            [MessagePackObject(AllowPrivate = true)]
+            public partial class SampleObject
+            {
+                [Key(0)]
+                [MessagePackFormatter(typeof(ValueTupleFormatter<string>))]
+                private ValueTuple<string> TupleTest;
+            }
+
+            public sealed class ValueTupleFormatter<T1> : IMessagePackFormatter<ValueTuple<T1>>
+            {
+                public void Serialize(ref MessagePackWriter writer, ValueTuple<T1> value, MessagePackSerializerOptions options)
+                {
+                    throw new NotImplementedException();
+                }
+
+                public ValueTuple<T1> Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
+                {
+                    throw new NotImplementedException();
+                }
+            }
+            """;
+
+        await VerifyCS.Test.RunDefaultAsync(this.testOutputHelper, testSource);
+    }
 }
diff --git a/tests/MessagePack.SourceGenerator.Tests/Resources/GenericCustomFormatter/Formatters.SampleObject.SampleObjectFormatter.g.cs b/tests/MessagePack.SourceGenerator.Tests/Resources/GenericCustomFormatter/Formatters.SampleObject.SampleObjectFormatter.g.cs
new file mode 100644
index 000000000..7f389d0d7
--- /dev/null
+++ b/tests/MessagePack.SourceGenerator.Tests/Resources/GenericCustomFormatter/Formatters.SampleObject.SampleObjectFormatter.g.cs
@@ -0,0 +1,55 @@
+﻿// <auto-generated />
+
+#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;
+
+partial class SampleObject {
+
+	internal sealed class SampleObjectFormatter : MsgPack::Formatters.IMessagePackFormatter<global::SampleObject>
+	{
+		private readonly global::ValueTupleFormatter<string> __TupleTestCustomFormatter__ = new global::ValueTupleFormatter<string>();
+
+		public void Serialize(ref MsgPack::MessagePackWriter writer, global::SampleObject value, MsgPack::MessagePackSerializerOptions options)
+		{
+			if (value == null)
+			{
+				writer.WriteNil();
+				return;
+			}
+
+			writer.WriteArrayHeader(1);
+			this.__TupleTestCustomFormatter__.Serialize(ref writer, value.TupleTest, options);
+		}
+
+		public global::SampleObject Deserialize(ref MsgPack::MessagePackReader reader, MsgPack::MessagePackSerializerOptions options)
+		{
+			if (reader.TryReadNil())
+			{
+				return null;
+			}
+
+			options.Security.DepthStep(ref reader);
+			var length = reader.ReadArrayHeader();
+			var ____result = new global::SampleObject();
+
+			for (int i = 0; i < length; i++)
+			{
+				switch (i)
+				{
+					case 0:
+						____result.TupleTest = this.__TupleTestCustomFormatter__.Deserialize(ref reader, options);
+						break;
+					default:
+						reader.Skip();
+						break;
+				}
+			}
+
+			reader.Depth--;
+			return ____result;
+		}
+	}
+}
diff --git a/tests/MessagePack.SourceGenerator.Tests/Resources/GenericCustomFormatter/MessagePack.GeneratedMessagePackResolver.g.cs b/tests/MessagePack.SourceGenerator.Tests/Resources/GenericCustomFormatter/MessagePack.GeneratedMessagePackResolver.g.cs
new file mode 100644
index 000000000..ea583ace1
--- /dev/null
+++ b/tests/MessagePack.SourceGenerator.Tests/Resources/GenericCustomFormatter/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 global::System.Collections.Generic.Dictionary<global::System.Type, int>(1)
+		{
+			{ typeof(global::SampleObject), 0 },
+		};
+
+		internal static object GetFormatter(global::System.Type t)
+		{
+			if (closedTypeLookup.TryGetValue(t, out int closedKey))
+			{
+				switch (closedKey)
+				{
+					case 0: return new global::SampleObject.SampleObjectFormatter();
+					default: return null; // unreachable
+				};
+			}
+
+			return null;
+		}
+	}
+}
+
+}